Skip to content

Commit

Permalink
refactor: queries usage and add semi-auto mutation invalidation
Browse files Browse the repository at this point in the history
  • Loading branch information
PetrBulanek authored Jan 16, 2025
1 parent fc3c8f4 commit bc251fc
Show file tree
Hide file tree
Showing 89 changed files with 1,304 additions and 1,545 deletions.
2 changes: 1 addition & 1 deletion src/app/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import { UserMetadata, UserProfileState } from '@/store/user-profile/types';
import { checkErrorCode } from '@/utils/handleApiError';
import { noop } from '@/utils/helpers';
import NextAuth from 'next-auth';
import type { OAuthConfig } from 'next-auth/providers';
import * as z from 'zod';
Expand All @@ -27,7 +28,6 @@ import {
encodeMetadata,
maybeGetJsonBody,
} from '../api/utils';
import { noop } from '@/utils/helpers';

const AUTH_PROVIDER_ID = process.env.NEXT_PUBLIC_AUTH_PROVIDER_ID!;
const AUTH_PROVIDER_NAME = process.env.AUTH_PROVIDER_NAME!;
Expand Down
19 changes: 10 additions & 9 deletions src/layout/providers/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ import { Organization } from '@/app/api/organization/types';
import { ProjectUser } from '@/app/api/projects-users/types';
import { Project } from '@/app/api/projects/types';
import { encodeEntityWithMetadata } from '@/app/api/utils';
import { readAssistantQuery } from '@/modules/assistants/queries';
import { getAssistantsQueries } from '@/modules/assistants/queries';
import { Assistant } from '@/modules/assistants/types';
import { readProjectQuery } from '@/modules/projects/queries';
import { readProjectUserQuery } from '@/modules/projects/users/queries';
import { getProjectsQueries } from '@/modules/projects/queries';
import { getProjectUsersQueries } from '@/modules/projects/users/queries';
import { useUserProfile } from '@/store/user-profile';
import { FeatureName } from '@/utils/parseFeatureFlags';
import { useQuery } from '@tanstack/react-query';
import {
createContext,
Expand All @@ -34,7 +35,6 @@ import {
useRef,
useState,
} from 'react';
import { FeatureName } from '@/utils/parseFeatureFlags';

export interface AppContextValue {
assistant: Assistant | null;
Expand All @@ -60,8 +60,6 @@ const AppApiContext = createContext<AppApiContextValue>(
null as unknown as AppApiContextValue,
);

const ProjectContext = createContext<Props>(null as unknown as Props);

interface Props {
featureFlags: Record<FeatureName, boolean>;
project: Project;
Expand All @@ -78,22 +76,25 @@ export function AppProvider({
const [assistant, setAssistant] = useState<Assistant | null>(null);
const onPageLeaveRef = useRef(() => null);
const userId = useUserProfile((state) => state.id);
const projectsQueries = getProjectsQueries({ organization });
const assistantsQueries = getAssistantsQueries({ organization, project });
const projectUsersQueries = getProjectUsersQueries({ organization });

const { data: projectData } = useQuery({
...readProjectQuery(organization.id, project.id),
...projectsQueries.detail(project.id),
initialData: project,
});

const { data: assistantData } = useQuery({
...readAssistantQuery(organization.id, project.id, assistant?.id ?? ''),
...assistantsQueries.detail(assistant?.id ?? ''),
enabled: Boolean(assistant),
initialData: assistant
? encodeEntityWithMetadata<Assistant>(assistant)
: undefined,
});

const { data: projectUser } = useQuery({
...readProjectUserQuery(organization.id, project.id, userId),
...projectUsersQueries.detail(project.id, userId),
enabled: Boolean(userId),
});

Expand Down
69 changes: 38 additions & 31 deletions src/layout/providers/QueryProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
'use client';

import {
matchQuery,
MutationCache,
QueryCache,
QueryClient,
QueryClientConfig,
QueryClientProvider,
QueryKey,
} from '@tanstack/react-query';
import { PropsWithChildren } from 'react';
import { useHandleError } from '../hooks/useHandleError';
Expand All @@ -33,70 +35,75 @@ interface QueryMetadata extends Record<string, unknown> {
declare module '@tanstack/react-query' {
interface Register {
queryMeta: QueryMetadata;
mutationMeta: QueryMetadata;
mutationMeta: QueryMetadata & {
invalidates?: QueryKey[];
};
}
}

function makeQueryClient(
handleError: HandleErrorFn,
config: Omit<QueryClientConfig, 'defaultOptions'> = {},
) {
return new QueryClient({
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
queryCache: new QueryCache({
onError(error, query) {
handleError(error, {
toast: query.meta?.errorToast,
});
},
}),
mutationCache: new MutationCache({
onError(error, variables, context, mutation) {
handleError(error, {
toast: mutation.meta?.errorToast,
});
},
onSuccess(data, variables, context, mutation) {
queryClient.invalidateQueries({
predicate: (query) => {
return (
mutation.meta?.invalidates?.some((queryKey) =>
matchQuery({ queryKey }, query),
) ?? false
);
},
});
},
}),
...config,
});

return queryClient;
}

let browserQueryClient: QueryClient | undefined = undefined;

function getQueryClient(handleError: HandleErrorFn) {
if (typeof window === 'undefined') {
// Server: always make a new query client
return makeQueryClient({
queryCache: getQueryCache(handleError),
mutationCache: getMutationCache(handleError),
});
return makeQueryClient(handleError);
} else {
// Browser: make a new query client if we don't already have one
// This is very important so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient)
browserQueryClient = makeQueryClient({
queryCache: getQueryCache(handleError),
mutationCache: getMutationCache(handleError),
});
if (!browserQueryClient) {
browserQueryClient = makeQueryClient(handleError);
}
return browserQueryClient;
}
}

type HandleErrorFn = ReturnType<typeof useHandleError>;

function getQueryCache(handleError: HandleErrorFn) {
return new QueryCache({
onError(error, query) {
handleError(error, {
toast: query.meta?.errorToast,
});
},
});
}

function getMutationCache(handleError: HandleErrorFn) {
return new MutationCache({
onError(error, variables, context, mutation) {
handleError(error, {
toast: mutation.meta?.errorToast,
});
},
});
}

export function QueryProvider({ children }: PropsWithChildren) {
const handleError = useHandleError();
// NOTE: Avoid useState when initializing the query client if you don't
Expand Down
75 changes: 19 additions & 56 deletions src/modules/api-keys/ApiKeysHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@
*/

'use client';
import { EmptyDataInfo } from '@/components/CardsList/CardsList';
import { DateTime } from '@/components/DateTime/DateTime';
import { InlineEditableField } from '@/components/InlineEditableField/InlineEditableField';
import { TablePagination } from '@/components/TablePagination/TablePagination';
import { usePagination } from '@/components/TablePagination/usePagination';
import { useDataResultState } from '@/hooks/useDataResultState';
import { useModal } from '@/layout/providers/ModalProvider';
import { useUserProfile } from '@/store/user-profile';
import {
Button,
DataTable,
Expand All @@ -33,28 +39,19 @@ import {
TableToolbarContent,
TableToolbarSearch,
} from '@carbon/react';
import { useQuery } from '@tanstack/react-query';
import { useAppContext } from '@/layout/providers/AppProvider';
import { useEffect, useId, useMemo } from 'react';
import { useDebounceValue } from 'usehooks-ts';
import {
PreferencesLayout,
PreferencesSection,
} from '../preferences/PreferencesLayout';
import { apiKeysQuery } from './api/queries';
import { useApiKeys } from './api/useApiKeys';
import { useRenameApiKey } from './api/useRenameApiKey';
import { useDebounceValue } from 'usehooks-ts';
import { DateTime } from '@/components/DateTime/DateTime';
import { useDataResultState } from '@/hooks/useDataResultState';
import { TablePagination } from '@/components/TablePagination/TablePagination';
import classes from './ApiKeysHome.module.scss';
import { ApiKeyModal } from './manage/ApiKeyModal';
import { usePagination } from '@/components/TablePagination/usePagination';
import { useUserProfile } from '@/store/user-profile';
import { EmptyDataInfo } from '@/components/CardsList/CardsList';

export function ApiKeysHome() {
const id = useId();
const { project, organization } = useAppContext();
const { openModal, openConfirmation } = useModal();
const [search, setSearch] = useDebounceValue('', 200);
const userId = useUserProfile((state) => state.id);
Expand All @@ -73,26 +70,21 @@ export function ApiKeysHome() {
resetPagination();
}, [resetPagination, search]);

const { data, isPending, isFetching } = useQuery(
apiKeysQuery(organization.id, {
search,
limit: PAGE_SIZE,
after,
before,
}),
);
const { data, isPending, isFetching } = useApiKeys({
params: { search, limit: PAGE_SIZE, after, before },
});

const { isEmpty } = useDataResultState({
totalCount: data?.total_count,
isFetching,
isFiltered: Boolean(search),
});

const { mutate: mutateRename } = useRenameApiKey({});
const { mutate: mutateRename } = useRenameApiKey();

const rows = useMemo(
() =>
data?.data?.map((item, index) => {
data?.data?.map((item) => {
const { id, name, secret, created_at, last_used_at, project, owner } =
item;
return {
Expand All @@ -102,12 +94,7 @@ export function ApiKeysHome() {
defaultValue={name}
required
onConfirm={(value) =>
mutateRename({
id,
projectId: project.id,
organizationId: organization.id,
name: value,
})
mutateRename({ projectId: project.id, id, name: value })
}
/>
),
Expand All @@ -132,11 +119,7 @@ export function ApiKeysHome() {
primaryButtonText: 'Regenerate',
onSubmit: () =>
openModal((props) => (
<ApiKeyModal.Regenerate
organization={organization}
apiKey={item}
{...props}
/>
<ApiKeyModal.Regenerate apiKey={item} {...props} />
)),
})
}
Expand All @@ -146,26 +129,15 @@ export function ApiKeysHome() {
itemText="Delete"
onClick={() =>
openModal((props) => (
<ApiKeyModal.Delete
organization={organization}
apiKey={item}
{...props}
/>
<ApiKeyModal.Delete apiKey={item} {...props} />
))
}
/>
</OverflowMenu>
),
};
}) ?? [],
[
data?.data,
mutateRename,
openConfirmation,
openModal,
userId,
organization,
],
[data?.data, mutateRename, openConfirmation, openModal, userId],
);

return (
Expand All @@ -180,14 +152,7 @@ export function ApiKeysHome() {
<EmptyDataInfo
newButtonProps={{
title: 'Create API key',
onClick: () =>
openModal((props) => (
<ApiKeyModal
{...props}
organization={organization}
project={project}
/>
)),
onClick: () => openModal((props) => <ApiKeyModal {...props} />),
}}
isEmpty={isEmpty}
noItemsInfo="You haven't created any API keys yet."
Expand Down Expand Up @@ -216,10 +181,8 @@ export function ApiKeysHome() {
onClick={() =>
openModal((props) => (
<ApiKeyModal
organization={organization}
{...props}
project={project}
onSuccess={() => resetPagination()}
{...props}
/>
))
}
Expand Down
Loading

0 comments on commit bc251fc

Please sign in to comment.