-
Notifications
You must be signed in to change notification settings - Fork 948
fix(site): tighten interface design for various frontend utility functions #18894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bb05088
a119d70
5dc67cf
4c77bff
4833ae1
b8d92fa
1662a55
e326414
cc00106
7304644
b512e18
cb92b79
86d19b8
330d91d
fb9d8f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,21 +48,6 @@ export const templates = ( | |
}; | ||
}; | ||
|
||
const getTemplatesByOrganizationQueryKey = ( | ||
organization: string, | ||
options?: GetTemplatesOptions, | ||
) => [organization, "templates", options?.deprecated]; | ||
|
||
const templatesByOrganization = ( | ||
organization: string, | ||
options: GetTemplatesOptions = {}, | ||
) => { | ||
return { | ||
queryKey: getTemplatesByOrganizationQueryKey(organization, options), | ||
queryFn: () => API.getTemplatesByOrganization(organization, options), | ||
}; | ||
}; | ||
|
||
export const templateACL = (templateId: string) => { | ||
return { | ||
queryKey: ["templateAcl", templateId], | ||
|
@@ -121,9 +106,11 @@ export const templateExamples = () => { | |
}; | ||
}; | ||
|
||
export const templateVersionRoot: string = "templateVersion"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This ends up being more important for the upcoming PR, but I really, really think that we need to find the time to formalize our entire data layer, and be way more deliberate with our React Query caching I fully expect our caching to create a super, super hard-to-recreate bug sometime in the future, because it's a bunch of semi-structured global mutable state right now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also added an explicit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: there seems to be inconsistencies within the code base between using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that's fair. The JS/TS way seems to be SCREAMING_CASE for constants, while the Go way seems to be to treat them as any other package value I'm leaning towards switching this to SCREAMING_CASE, but I'll wait for Kayla to chime in |
||
|
||
export const templateVersion = (versionId: string) => { | ||
return { | ||
queryKey: ["templateVersion", versionId], | ||
queryKey: [templateVersionRoot, versionId], | ||
queryFn: () => API.getTemplateVersion(versionId), | ||
}; | ||
}; | ||
|
@@ -134,7 +121,7 @@ export const templateVersionByName = ( | |
versionName: string, | ||
) => { | ||
return { | ||
queryKey: ["templateVersion", organizationId, templateName, versionName], | ||
queryKey: [templateVersionRoot, organizationId, templateName, versionName], | ||
queryFn: () => | ||
API.getTemplateVersionByName(organizationId, templateName, versionName), | ||
}; | ||
|
@@ -153,7 +140,7 @@ export const templateVersions = (templateId: string) => { | |
}; | ||
|
||
export const templateVersionVariablesKey = (versionId: string) => [ | ||
"templateVersion", | ||
templateVersionRoot, | ||
versionId, | ||
"variables", | ||
]; | ||
|
@@ -216,7 +203,7 @@ export const templaceACLAvailable = ( | |
}; | ||
|
||
const templateVersionExternalAuthKey = (versionId: string) => [ | ||
"templateVersion", | ||
templateVersionRoot, | ||
versionId, | ||
"externalAuth", | ||
]; | ||
|
@@ -257,21 +244,21 @@ const createTemplateFn = async (options: CreateTemplateOptions) => { | |
|
||
export const templateVersionLogs = (versionId: string) => { | ||
return { | ||
queryKey: ["templateVersion", versionId, "logs"], | ||
queryKey: [templateVersionRoot, versionId, "logs"], | ||
queryFn: () => API.getTemplateVersionLogs(versionId), | ||
}; | ||
}; | ||
|
||
export const richParameters = (versionId: string) => { | ||
return { | ||
queryKey: ["templateVersion", versionId, "richParameters"], | ||
queryKey: [templateVersionRoot, versionId, "richParameters"], | ||
queryFn: () => API.getTemplateVersionRichParameters(versionId), | ||
}; | ||
}; | ||
|
||
export const resources = (versionId: string) => { | ||
return { | ||
queryKey: ["templateVersion", versionId, "resources"], | ||
queryKey: [templateVersionRoot, versionId, "resources"], | ||
queryFn: () => API.getTemplateVersionResources(versionId), | ||
}; | ||
}; | ||
|
@@ -293,7 +280,7 @@ export const previousTemplateVersion = ( | |
) => { | ||
return { | ||
queryKey: [ | ||
"templateVersion", | ||
templateVersionRoot, | ||
organizationId, | ||
templateName, | ||
versionName, | ||
|
@@ -313,7 +300,7 @@ export const previousTemplateVersion = ( | |
|
||
export const templateVersionPresets = (versionId: string) => { | ||
return { | ||
queryKey: ["templateVersion", versionId, "presets"], | ||
queryKey: [templateVersionRoot, versionId, "presets"], | ||
queryFn: () => API.getTemplateVersionPresets(versionId), | ||
}; | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,6 @@ import { useDebouncedFunction } from "hooks/debounce"; | |
import { ExternalLinkIcon } from "lucide-react"; | ||
import { ChevronDownIcon } from "lucide-react"; | ||
import { type FC, type ReactNode, useEffect, useRef, useState } from "react"; | ||
import type { useSearchParams } from "react-router-dom"; | ||
|
||
type PresetFilter = { | ||
name: string; | ||
|
@@ -27,35 +26,55 @@ type FilterValues = Record<string, string | undefined>; | |
|
||
type UseFilterConfig = { | ||
/** | ||
* The fallback value to use in the event that no filter params can be parsed | ||
* from the search params object. This value is allowed to change on | ||
* re-renders. | ||
* The fallback value to use in the event that no filter params can be | ||
* parsed from the search params object. | ||
*/ | ||
fallbackFilter?: string; | ||
searchParamsResult: ReturnType<typeof useSearchParams>; | ||
searchParams: URLSearchParams; | ||
onSearchParamsChange: (newParams: URLSearchParams) => void; | ||
Comment on lines
+33
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Decoupled type signature from React Router. Splitting this up gives us a lot more freedom, which comes up later in this PR |
||
onUpdate?: (newValue: string) => void; | ||
}; | ||
|
||
export type UseFilterResult = Readonly<{ | ||
query: string; | ||
values: FilterValues; | ||
used: boolean; | ||
update: (newValues: string | FilterValues) => void; | ||
debounceUpdate: (newValues: string | FilterValues) => void; | ||
cancelDebounce: () => void; | ||
}>; | ||
Comment on lines
+38
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like the previous type was super, super risky, because it was derived from a function that didn't have much type-safety With this, the contract is way more explicit and simple, and there's basically no chance of the TypeScript LSP getting confused and dropping properties for tooltips |
||
|
||
export const useFilterParamsKey = "filter"; | ||
|
||
export const useFilter = ({ | ||
fallbackFilter = "", | ||
searchParamsResult, | ||
searchParams, | ||
onSearchParamsChange, | ||
onUpdate, | ||
}: UseFilterConfig) => { | ||
const [searchParams, setSearchParams] = searchParamsResult; | ||
}: UseFilterConfig): UseFilterResult => { | ||
const query = searchParams.get(useFilterParamsKey) ?? fallbackFilter; | ||
|
||
const update = (newValues: string | FilterValues) => { | ||
const serialized = | ||
typeof newValues === "string" ? newValues : stringifyFilter(newValues); | ||
const noUpdateNeeded = query === serialized; | ||
if (noUpdateNeeded) { | ||
return; | ||
} | ||
|
||
/** | ||
* @todo 2025-07-15 - We have a slightly nasty bug here, where trying to | ||
* update state the "React way" causes our code to break. | ||
* | ||
* In theory, it would be better to make a copy of the search params. We | ||
* can then mutate and dispatch the copy instead of the original. Doing | ||
* that causes other parts of our existing logic to break, though. | ||
* That's a sign that our other code is slightly broken, and only just | ||
* happens to work by chance right now. | ||
*/ | ||
searchParams.set(useFilterParamsKey, serialized); | ||
setSearchParams(searchParams); | ||
|
||
if (onUpdate !== undefined) { | ||
onUpdate(serialized); | ||
} | ||
onSearchParamsChange(searchParams); | ||
onUpdate?.(serialized); | ||
}; | ||
|
||
const { debounced: debounceUpdate, cancelDebounce } = useDebouncedFunction( | ||
|
@@ -73,8 +92,6 @@ export const useFilter = ({ | |
}; | ||
}; | ||
|
||
export type UseFilterResult = ReturnType<typeof useFilter>; | ||
|
||
const parseFilterQuery = (filterQuery: string): FilterValues => { | ||
if (filterQuery === "") { | ||
return {}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,41 @@ | ||
import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils"; | ||
import type { useSearchParams } from "react-router-dom"; | ||
|
||
export const usePagination = ({ | ||
searchParamsResult, | ||
}: { | ||
searchParamsResult: ReturnType<typeof useSearchParams>; | ||
}) => { | ||
const [searchParams, setSearchParams] = searchParamsResult; | ||
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1; | ||
const limit = DEFAULT_RECORDS_PER_PAGE; | ||
const offset = page <= 0 ? 0 : (page - 1) * limit; | ||
const paginationKey = "page"; | ||
|
||
const goToPage = (page: number) => { | ||
searchParams.set("page", page.toString()); | ||
setSearchParams(searchParams); | ||
}; | ||
type UsePaginationOptions = Readonly<{ | ||
searchParams: URLSearchParams; | ||
onSearchParamsChange: (newParams: URLSearchParams) => void; | ||
}>; | ||
|
||
type UsePaginationResult = Readonly<{ | ||
page: number; | ||
limit: number; | ||
offset: number; | ||
goToPage: (page: number) => void; | ||
}>; | ||
Comment on lines
+10
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We were missing a return type altogether before |
||
|
||
export function usePagination( | ||
options: UsePaginationOptions, | ||
): UsePaginationResult { | ||
const { searchParams, onSearchParamsChange } = options; | ||
const limit = DEFAULT_RECORDS_PER_PAGE; | ||
const rawPage = Number.parseInt(searchParams.get(paginationKey) || "1", 10); | ||
const page = Number.isNaN(rawPage) || rawPage <= 0 ? 1 : rawPage; | ||
|
||
return { | ||
page, | ||
limit, | ||
goToPage, | ||
offset, | ||
offset: Math.max(0, (page - 1) * limit), | ||
goToPage: (newPage) => { | ||
const abortNavigation = | ||
page === newPage || !Number.isFinite(page) || !Number.isInteger(page); | ||
if (abortNavigation) { | ||
return; | ||
} | ||
Comment on lines
+30
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another render optimization There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this render optimization? Not re-rendering the page if it's the same page? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So there's two parts:
I could maybe add a comment, but React only has |
||
|
||
const copy = new URLSearchParams(searchParams); | ||
copy.set("page", page.toString()); | ||
onSearchParamsChange(copy); | ||
}, | ||
}; | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -108,9 +108,7 @@ export const CreateWorkspacePageViewExperimental: FC< | |
owner, | ||
setOwner, | ||
}) => { | ||
const [suggestedName, setSuggestedName] = useState(() => | ||
generateWorkspaceName(), | ||
); | ||
const [suggestedName, setSuggestedName] = useState(generateWorkspaceName); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The extra wrapper function was redundant |
||
const [showPresetParameters, setShowPresetParameters] = useState(false); | ||
const id = useId(); | ||
const workspaceNameInputRef = useRef<HTMLInputElement>(null); | ||
|
@@ -124,14 +122,14 @@ export const CreateWorkspacePageViewExperimental: FC< | |
|
||
// Only touched fields are sent to the websocket | ||
// Autofilled parameters are marked as touched since they have been modified | ||
const initialTouched = parameters.reduce( | ||
const initialTouched = parameters.reduce<Record<string, boolean>>( | ||
(touched, parameter) => { | ||
if (autofillByName[parameter.name] !== undefined) { | ||
touched[parameter.name] = true; | ||
} | ||
return touched; | ||
}, | ||
{} as Record<string, boolean>, | ||
{}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed type assertion to remove risk of accidentally silencing the compiler |
||
); | ||
|
||
// The form parameters values hold the working state of the parameters that will be submitted when creating a workspace | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weren't used at all