Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
Parkreiner wants to merge15 commits intomain
base:main
Choose a base branch
Loading
frommes/batch-update-01
Open
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
15 commits
Select commitHold shift + click to select a range
bb05088
refactor: centralize storybook queries key
ParkreinerJul 16, 2025
a119d70
refactor: centralize template version key prefix
ParkreinerJul 16, 2025
5dc67cf
refactor: remove JSX extension from batch actions
ParkreinerJul 16, 2025
4c77bff
fix: update API design of various utility hooks
ParkreinerJul 16, 2025
4833ae1
fix: remove promise race conditions for batch utilities
ParkreinerJul 16, 2025
b8d92fa
fix: decouple component props from one another
ParkreinerJul 16, 2025
1662a55
fix: apply biome fixes
ParkreinerJul 16, 2025
e326414
refactor: remove more bad coupling
ParkreinerJul 16, 2025
cc00106
fix: format
ParkreinerJul 16, 2025
7304644
fix: update call site mismatch
ParkreinerJul 16, 2025
b512e18
fix: update import
ParkreinerJul 16, 2025
cb92b79
fix: update Filter logic to account for fallback filter
ParkreinerJul 16, 2025
86d19b8
docs: add comment about wonky code
ParkreinerJul 16, 2025
330d91d
fix: get workspace tests passing
ParkreinerJul 16, 2025
fb9d8f2
fix: update biome again
ParkreinerJul 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletionssite/.storybook/preview.jsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,13 @@ function withHelmet(Story) {
);
}

/**
* This JSX file isn't part of the main project, so it doesn't get to use the
* ambient types defined in `storybook.d.ts` to provide extra type-safety.
* Extracting main key to avoid typos.
*/
const queryParametersKey = "queries";

/** @type {Decorator} */
function withQuery(Story, { parameters }) {
const queryClient = new QueryClient({
Expand All@@ -112,8 +119,8 @@ function withQuery(Story, { parameters }) {
},
});

if (parameters.queries) {
for (const query of parameters.queries) {
if (parameters[queryParametersKey]) {
for (const query of parameters[queryParametersKey]) {
queryClient.setQueryData(query.key, query.data);
}
}
Expand Down
35 changes: 11 additions & 24 deletionssite/src/api/queries/templates.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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),
};
};
Comment on lines -51 to -64
Copy link
MemberAuthor

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


export const templateACL = (templateId: string) => {
return {
queryKey: ["templateAcl", templateId],
Expand DownExpand Up@@ -121,9 +106,11 @@ export const templateExamples = () => {
};
};

export const templateVersionRoot: string = "templateVersion";
Copy link
MemberAuthor

Choose a reason for hiding this comment

The 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

Copy link
MemberAuthor

@ParkreinerParkreinerJul 16, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Also added an explicitstring type annotation so that TypeScript doesn't over-infer the value and treat it as a literal. We shouldn't know what exact combination of characters it is – just what purpose the string serves in the greater codebase as an opaque value

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

nit: there seems to be inconsistencies within the code base between usingcamelCase orUPPER_SNAKE_CASE for file-wide constants so while out of scope for this PR I think deciding on one and sticking to it would be good

Copy link
MemberAuthor

Choose a reason for hiding this comment

The 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),
};
};
Expand All@@ -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),
};
Expand All@@ -153,7 +140,7 @@ export const templateVersions = (templateId: string) => {
};

export const templateVersionVariablesKey = (versionId: string) => [
"templateVersion",
templateVersionRoot,
versionId,
"variables",
];
Expand DownExpand Up@@ -216,7 +203,7 @@ export const templaceACLAvailable = (
};

const templateVersionExternalAuthKey = (versionId: string) => [
"templateVersion",
templateVersionRoot,
versionId,
"externalAuth",
];
Expand DownExpand Up@@ -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),
};
};
Expand All@@ -293,7 +280,7 @@ export const previousTemplateVersion = (
) => {
return {
queryKey: [
"templateVersion",
templateVersionRoot,
organizationId,
templateName,
versionName,
Expand All@@ -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),
};
};
Expand Down
47 changes: 32 additions & 15 deletionssite/src/components/Filter/Filter.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand All@@ -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
Copy link
MemberAuthor

Choose a reason for hiding this comment

The 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
Copy link
MemberAuthor

@ParkreinerParkreinerJul 16, 2025
edited
Loading

Choose a reason for hiding this comment

The 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(
Expand All@@ -73,8 +92,6 @@ export const useFilter = ({
};
};

export type UseFilterResult = ReturnType<typeof useFilter>;

const parseFilterQuery = (filterQuery: string): FilterValues => {
if (filterQuery === "") {
return {};
Expand Down
50 changes: 33 additions & 17 deletionssite/src/hooks/usePagination.ts
View file
Open in desktop
Original file line numberDiff line numberDiff 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
Copy link
MemberAuthor

Choose a reason for hiding this comment

The 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
Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Another render optimization

Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
MemberAuthor

@ParkreinerParkreinerJul 16, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

So there's two parts:

  • The basic input validation from theNumber static methods
  • The equality check

I could maybe add a comment, but React only hasObject.is for change detection, so in general, a lot of render optimizations boil down to "only dispatch a new value (especially a non-primitive value) when you know the contents have changed". Most of the time, you don't have to worry about over-dispatching primitive values (as long as they're done outside a render, React will detect that nothing changed and do a no-op for the state update). But because JS is wacky, there are edge cases withObject.is and numbers, which is why there's the extra=== check


const copy = new URLSearchParams(searchParams);
copy.set("page", page.toString());
onSearchParamsChange(copy);
},
};
};
}
3 changes: 2 additions & 1 deletionsite/src/pages/AuditPage/AuditPage.test.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { API } from "api/api";
import type { AuditLogsRequest } from "api/typesGenerated";
import { DEFAULT_RECORDS_PER_PAGE } from "components/PaginationWidget/utils";
import { http, HttpResponse } from "msw";
import {
Expand DownExpand Up@@ -106,7 +107,7 @@ describe("AuditPage", () => {
await userEvent.type(filterField, query);

await waitFor(() =>
expect(getAuditLogsSpy).toBeCalledWith({
expect(getAuditLogsSpy).toHaveBeenCalledWith<[AuditLogsRequest]>({
limit: DEFAULT_RECORDS_PER_PAGE,
offset: 0,
q: query,
Expand Down
3 changes: 2 additions & 1 deletionsite/src/pages/AuditPage/AuditPage.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,7 +33,8 @@ const AuditPage: FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
const auditsQuery = usePaginatedQuery(paginatedAudits(searchParams));
const filter = useFilter({
searchParamsResult: [searchParams, setSearchParams],
searchParams,
onSearchParamsChange: setSearchParams,
onUpdate: auditsQuery.goToFirstPage,
});

Expand Down
3 changes: 2 additions & 1 deletionsite/src/pages/ConnectionLogPage/ConnectionLogPage.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,8 @@ const ConnectionLogPage: FC = () => {
paginatedConnectionLogs(searchParams),
);
const filter = useFilter({
searchParamsResult: [searchParams, setSearchParams],
searchParams,
onSearchParamsChange: setSearchParams,
onUpdate: connectionlogsQuery.goToFirstPage,
});

Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -108,9 +108,7 @@ export const CreateWorkspacePageViewExperimental: FC<
owner,
setOwner,
}) => {
const [suggestedName, setSuggestedName] = useState(() =>
generateWorkspaceName(),
);
const [suggestedName, setSuggestedName] = useState(generateWorkspaceName);
Copy link
MemberAuthor

@ParkreinerParkreinerJul 16, 2025
edited
Loading

Choose a reason for hiding this comment

The 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);
Expand All@@ -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>,
{},
Copy link
MemberAuthor

Choose a reason for hiding this comment

The 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
Expand Down
5 changes: 3 additions & 2 deletionssite/src/pages/TemplatesPage/TemplatesPage.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,10 +14,11 @@ const TemplatesPage: FC = () => {
const { permissions, user: me } = useAuthenticated();
const { showOrganizations } = useDashboard();

constsearchParamsResult = useSearchParams();
const[searchParams, setSearchParams] = useSearchParams();
const filter = useFilter({
fallbackFilter: "deprecated:false",
searchParamsResult,
searchParams,
onSearchParamsChange: setSearchParams,
onUpdate: () => {}, // reset pagination
});

Expand Down
8 changes: 4 additions & 4 deletionssite/src/pages/UsersPage/UsersPage.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,9 +39,8 @@ type UserPageProps = {
const UsersPage: FC<UserPageProps> = ({ defaultNewPassword }) => {
const queryClient = useQueryClient();
const navigate = useNavigate();
constsearchParamsResult = useSearchParams();
const[searchParams, setSearchParams] = useSearchParams();
const { entitlements } = useDashboard();
const [searchParams] = searchParamsResult;

const groupsByUserIdQuery = useQuery(groupsByUserId());
const authMethodsQuery = useQuery(authMethods());
Expand All@@ -58,9 +57,10 @@ const UsersPage: FC<UserPageProps> = ({ defaultNewPassword }) => {
enabled: viewDeploymentConfig,
});

const usersQuery = usePaginatedQuery(paginatedUsers(searchParamsResult[0]));
const usersQuery = usePaginatedQuery(paginatedUsers(searchParams));
const useFilterResult = useFilter({
searchParamsResult,
searchParams,
onSearchParamsChange: setSearchParams,
onUpdate: usersQuery.goToFirstPage,
});

Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp