diff --git a/ui-v2/src/components/concurrency/concurrency-constants.ts b/ui-v2/src/components/concurrency/concurrency-constants.ts deleted file mode 100644 index 2ffc86f41409..000000000000 --- a/ui-v2/src/components/concurrency/concurrency-constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const TAB_OPTIONS = { - Global: "Global", - "Task Run": "Task Run", -} as const; -export type TabOptions = keyof typeof TAB_OPTIONS; diff --git a/ui-v2/src/components/concurrency/concurrency-page.tsx b/ui-v2/src/components/concurrency/concurrency-page.tsx index 17012f025ba1..d513deec48e7 100644 --- a/ui-v2/src/components/concurrency/concurrency-page.tsx +++ b/ui-v2/src/components/concurrency/concurrency-page.tsx @@ -1,23 +1,15 @@ -import { useState } from "react"; +import { Typography } from "@/components/ui/typography"; -import { Typography } from "@/components//ui/typography"; - -import { TAB_OPTIONS, TabOptions } from "./concurrency-constants"; import { ConcurrencyTabs } from "./concurrency-tabs"; import { GlobalConcurrencyView } from "./global-concurrency-view"; import { TaskRunConcurrencyView } from "./task-run-concurrenct-view"; export const ConcurrencyPage = (): JSX.Element => { - // TODO: Use URL query instead - const [tab, setTab] = useState<TabOptions>(TAB_OPTIONS.Global); - return ( <div className="flex flex-col gap-4"> <Typography variant="h2">Concurrency</Typography> <div className="flex flex-col gap-6"> <ConcurrencyTabs - value={tab} - onValueChange={setTab} globalView={<GlobalConcurrencyView />} taskRunView={<TaskRunConcurrencyView />} /> diff --git a/ui-v2/src/components/concurrency/concurrency-tabs.tsx b/ui-v2/src/components/concurrency/concurrency-tabs.tsx index b3396114bca5..2c7fdd94d6b9 100644 --- a/ui-v2/src/components/concurrency/concurrency-tabs.tsx +++ b/ui-v2/src/components/concurrency/concurrency-tabs.tsx @@ -1,34 +1,81 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { TAB_OPTIONS, TabOptions } from "./concurrency-constants"; +import { TabOptions } from "@/routes/concurrency-limits"; +import { getRouteApi } from "@tanstack/react-router"; + +const routeApi = getRouteApi("/concurrency-limits"); + +type TabOptionValues = { + /** Value of search value in url */ + tabSearchValue: TabOptions; + /** Display value for the UI */ + displayValue: string; +}; + +/** Maps url tab option to visual name */ +const TAB_OPTIONS: Record<TabOptions, TabOptionValues> = { + global: { + tabSearchValue: "global", + displayValue: "Global", + }, + ["task-run"]: { + tabSearchValue: "task-run", + displayValue: "Task Run", + }, +} as const; type Props = { globalView: React.ReactNode; - onValueChange: (value: TabOptions) => void; taskRunView: React.ReactNode; - value: TabOptions; }; // TODO: Move Tabs for navigation to a generic styled component export const ConcurrencyTabs = ({ globalView, - onValueChange, taskRunView, - value, }: Props): JSX.Element => { + const { tab } = routeApi.useSearch(); + const navigate = routeApi.useNavigate(); + return ( - <Tabs - defaultValue="Global" - className="w-[400px]" - value={value} - onValueChange={(value) => onValueChange(value as TabOptions)} - > + <Tabs defaultValue="Global" value={tab}> <TabsList className="grid w-full grid-cols-2"> - <TabsTrigger value="Global">{TAB_OPTIONS.Global}</TabsTrigger> - <TabsTrigger value="Task Run">{TAB_OPTIONS["Task Run"]}</TabsTrigger> + <TabsTrigger + value={TAB_OPTIONS.global.tabSearchValue} + onClick={() => { + void navigate({ + to: "/concurrency-limits", + search: (prev) => ({ + ...prev, + tab: TAB_OPTIONS.global.tabSearchValue, + }), + }); + }} + > + {TAB_OPTIONS.global.displayValue} + </TabsTrigger> + + <TabsTrigger + value={TAB_OPTIONS["task-run"].tabSearchValue} + onClick={() => { + void navigate({ + to: "/concurrency-limits", + search: (prev) => ({ + ...prev, + tab: TAB_OPTIONS["task-run"].tabSearchValue, + }), + }); + }} + > + {TAB_OPTIONS["task-run"].displayValue} + </TabsTrigger> </TabsList> - <TabsContent value={TAB_OPTIONS.Global}>{globalView}</TabsContent> - <TabsContent value={TAB_OPTIONS["Task Run"]}>{taskRunView}</TabsContent> + <TabsContent value={TAB_OPTIONS.global.tabSearchValue}> + {globalView} + </TabsContent> + <TabsContent value={TAB_OPTIONS["task-run"].tabSearchValue}> + {taskRunView} + </TabsContent> </Tabs> ); }; diff --git a/ui-v2/src/components/concurrency/global-concurrency-view/index.tsx b/ui-v2/src/components/concurrency/global-concurrency-view/index.tsx index 20b5b7b78a63..033d5fb162ed 100644 --- a/ui-v2/src/components/concurrency/global-concurrency-view/index.tsx +++ b/ui-v2/src/components/concurrency/global-concurrency-view/index.tsx @@ -1,9 +1,13 @@ +import { useListGlobalConcurrencyLimits } from "@/hooks/global-concurrency-limits"; import { useState } from "react"; + import { GlobalConcurrencyLimitsHeader } from "./global-concurrency-limits-header"; export const GlobalConcurrencyView = () => { const [showAddDialog, setShowAddDialog] = useState(false); + const { data } = useListGlobalConcurrencyLimits(); + const openAddDialog = () => setShowAddDialog(true); const closeAddDialog = () => setShowAddDialog(false); @@ -12,6 +16,12 @@ export const GlobalConcurrencyView = () => { <div className="flex flex-col gap-2"> <GlobalConcurrencyLimitsHeader onAdd={openAddDialog} /> </div> + <div>TODO</div> + <ul> + {data.map((limit) => ( + <li key={limit.id}>{JSON.stringify(limit)}</li> + ))} + </ul> {showAddDialog && <div onClick={closeAddDialog}>TODO: DIALOG</div>} </> ); diff --git a/ui-v2/src/hooks/global-concurrency-limits.ts b/ui-v2/src/hooks/global-concurrency-limits.ts index 1fd78bb4fd23..cf3c5473a62e 100644 --- a/ui-v2/src/hooks/global-concurrency-limits.ts +++ b/ui-v2/src/hooks/global-concurrency-limits.ts @@ -1,10 +1,11 @@ import type { components } from "@/api/prefect"; import { getQueryService } from "@/api/service"; import { + QueryClient, queryOptions, useMutation, - useQuery, useQueryClient, + useSuspenseQuery, } from "@tanstack/react-query"; export type GlobalConcurrencyLimit = @@ -31,7 +32,7 @@ export const queryKeyFactory = { // ----- 🔑 Queries 🗄️ // ---------------------------- export const buildListGlobalConcurrencyLimitsQuery = ( - filter: GlobalConcurrencyLimitsFilter, + filter: GlobalConcurrencyLimitsFilter = { offset: 0 }, ) => queryOptions({ queryKey: queryKeyFactory.list(filter), @@ -47,11 +48,19 @@ export const buildListGlobalConcurrencyLimitsQuery = ( /** * * @param filter - * @returns list of global concurrency limits as a QueryResult object + * @returns list of global concurrency limits as a SuspenseQueryResult object */ + export const useListGlobalConcurrencyLimits = ( - filter: GlobalConcurrencyLimitsFilter, -) => useQuery(buildListGlobalConcurrencyLimitsQuery(filter)); + filter: GlobalConcurrencyLimitsFilter = { offset: 0 }, +) => useSuspenseQuery(buildListGlobalConcurrencyLimitsQuery(filter)); + +useListGlobalConcurrencyLimits.loader = ({ + context, +}: { + context: { queryClient: QueryClient }; +}) => + context.queryClient.ensureQueryData(buildListGlobalConcurrencyLimitsQuery()); // ----- ✍🏼 Mutations 🗄️ // ---------------------------- diff --git a/ui-v2/src/routes/concurrency-limits.tsx b/ui-v2/src/routes/concurrency-limits.tsx index e425901c1452..c5bfdaebe741 100644 --- a/ui-v2/src/routes/concurrency-limits.tsx +++ b/ui-v2/src/routes/concurrency-limits.tsx @@ -1,11 +1,24 @@ +import { ConcurrencyPage } from "@/components/concurrency/concurrency-page"; +import { useListGlobalConcurrencyLimits } from "@/hooks/global-concurrency-limits"; import { createFileRoute } from "@tanstack/react-router"; +import { zodSearchValidator } from "@tanstack/router-zod-adapter"; +import { z } from "zod"; -import { ConcurrencyPage } from "@/components/concurrency/concurrency-page"; +/** + * Schema for validating URL search parameters for the Concurrency Limits page. + * @property {'global' | 'task-run'} tab used designate which tab view to display + */ +const searchParams = z + .object({ + tab: z.enum(["global", "task-run"]).default("global"), + }) + .strict(); + +export type TabOptions = z.infer<typeof searchParams>["tab"]; export const Route = createFileRoute("/concurrency-limits")({ - component: RouteComponent, + validateSearch: zodSearchValidator(searchParams), + component: ConcurrencyPage, + wrapInSuspense: true, + loader: useListGlobalConcurrencyLimits.loader, }); - -function RouteComponent() { - return <ConcurrencyPage />; -}