From ff87355aba4741ee3043e12a18f25684c9c7ddc4 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Tue, 25 Feb 2025 19:06:47 +0200 Subject: [PATCH] POC/Provisioning: Unify tags (#101218) * Unify tags * add both tags * add tag types * Check for the redirect only once * Add fetch settings with delay hook * Refetch settings * Split hooks into separate files * Cleanup * Prettier * Prettier * Remove lint code * Betterer --------- Co-authored-by: Ryan McKinley --- .betterer.results | 14 +- pkg/registry/apis/provisioning/routes.go | 8 +- .../provisioning.grafana.app-v0alpha1.json | 6 +- .../features/provisioning/FileStatusPage.tsx | 96 +++++------- .../app/features/provisioning/JobStatus.tsx | 2 +- .../features/provisioning/OnboardingPage.tsx | 17 +- .../provisioning/RepositoryListPage.tsx | 4 +- .../Wizard/MigrateToProvisioningPage.tsx | 28 ++-- .../Wizard/ProvisioningWizard.tsx | 5 +- .../provisioning/api/endpoints.gen.ts | 4 +- public/app/features/provisioning/hooks.ts | 145 ------------------ .../app/features/provisioning/hooks/index.ts | 6 + .../hooks/useCreateOrUpdateRepository.ts | 40 +++++ .../hooks/useCreateOrUpdateRepositoryFile.ts | 22 +++ .../hooks/useGetResourceRepository.ts | 24 +++ .../provisioning/hooks/usePullRequestParam.ts | 12 ++ .../provisioning/hooks/useRepositoryJobs.ts | 32 ++++ .../provisioning/hooks/useRepositoryList.ts | 15 ++ 18 files changed, 248 insertions(+), 232 deletions(-) delete mode 100644 public/app/features/provisioning/hooks.ts create mode 100644 public/app/features/provisioning/hooks/index.ts create mode 100644 public/app/features/provisioning/hooks/useCreateOrUpdateRepository.ts create mode 100644 public/app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile.ts create mode 100644 public/app/features/provisioning/hooks/useGetResourceRepository.ts create mode 100644 public/app/features/provisioning/hooks/usePullRequestParam.ts create mode 100644 public/app/features/provisioning/hooks/useRepositoryJobs.ts create mode 100644 public/app/features/provisioning/hooks/useRepositoryList.ts diff --git a/.betterer.results b/.betterer.results index b6a79b98afd..bda601d29cd 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5756,8 +5756,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "6"], [0, 0, 0, "No untranslated strings. Wrap text with ", "7"], [0, 0, 0, "No untranslated strings. Wrap text with ", "8"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "10"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "9"] ], "public/app/features/provisioning/JobStatus.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], @@ -5870,8 +5869,7 @@ exports[`better eslint`] = { ], "public/app/features/provisioning/SyncRepository.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/provisioning/TokenPermissionsInfo.tsx:5381": [ [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], @@ -5942,6 +5940,14 @@ exports[`better eslint`] = { "public/app/features/provisioning/api/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], + "public/app/features/provisioning/hooks/index.ts:5381": [ + [0, 0, 0, "Do not re-export imported variable (\`./useCreateOrUpdateRepositoryFile\`)", "0"], + [0, 0, 0, "Do not re-export imported variable (\`./useCreateOrUpdateRepository\`)", "1"], + [0, 0, 0, "Do not re-export imported variable (\`./useGetResourceRepository\`)", "2"], + [0, 0, 0, "Do not re-export imported variable (\`./usePullRequestParam\`)", "3"], + [0, 0, 0, "Do not re-export imported variable (\`./useRepositoryJobs\`)", "4"], + [0, 0, 0, "Do not re-export imported variable (\`./useRepositoryList\`)", "5"] + ], "public/app/features/query/components/QueryEditorRow.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], diff --git a/pkg/registry/apis/provisioning/routes.go b/pkg/registry/apis/provisioning/routes.go index d119ead9b98..991bea682a7 100644 --- a/pkg/registry/apis/provisioning/routes.go +++ b/pkg/registry/apis/provisioning/routes.go @@ -25,8 +25,8 @@ func (b *APIBuilder) GetAPIRoutes() *builder.APIRoutes { Spec: &spec3.PathProps{ Get: &spec3.Operation{ OperationProps: spec3.OperationProps{ - OperationId: "getResourceStats", // used for RTK client - Tags: []string{"Provisioning"}, + OperationId: "getResourceStats", // used for RTK client + Tags: []string{"Provisioning", "Repository"}, //includes stats for repositores and provisiong in general Description: "Get resource stats for this namespace", Parameters: []*spec3.Parameter{ { @@ -72,7 +72,9 @@ func (b *APIBuilder) GetAPIRoutes() *builder.APIRoutes { Get: &spec3.Operation{ OperationProps: spec3.OperationProps{ OperationId: "getFrontendSettings", // used for RTK client - Tags: []string{"Provisioning"}, + //includes stats for repositores and provisiong in general + // This must include "Repository" so that the RTK client will invalidate when things are deleted + Tags: []string{"Provisioning", "Repository"}, Description: "Get the frontend settings for this namespace", Parameters: []*spec3.Parameter{ { diff --git a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json index ce3eeb043e9..8191474b1b8 100644 --- a/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/provisioning.grafana.app-v0alpha1.json @@ -2538,7 +2538,8 @@ "/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/settings": { "get": { "tags": [ - "Provisioning" + "Provisioning", + "Repository" ], "description": "Get the frontend settings for this namespace", "operationId": "getFrontendSettings", @@ -2570,7 +2571,8 @@ "/apis/provisioning.grafana.app/v0alpha1/namespaces/{namespace}/stats": { "get": { "tags": [ - "Provisioning" + "Provisioning", + "Repository" ], "description": "Get resource stats for this namespace", "operationId": "getResourceStats", diff --git a/public/app/features/provisioning/FileStatusPage.tsx b/public/app/features/provisioning/FileStatusPage.tsx index 752af3bd44b..c4b2a54c0c6 100644 --- a/public/app/features/provisioning/FileStatusPage.tsx +++ b/public/app/features/provisioning/FileStatusPage.tsx @@ -64,7 +64,6 @@ export default function FileStatusPage() { } enum TabSelection { - Lint = 'lint', File = 'file', Existing = 'existing', DryRun = 'dryRun', @@ -146,65 +145,44 @@ function ResourceView({ wrap, repo, repoRef, tab }: Props) { ))} - {tab === TabSelection.Lint && ( -
- {wrap.lint ? ( - wrap.lint.map((r, idx) => { - return ( - //@ts-expect-error TODO fix lint response types - - {r.message} - - ); - }) - ) : ( -
-

No lint issues

-
- )} +
+
+ + {({ height }) => ( + + )} +
- )} - - {tab !== TabSelection.Lint && ( -
-
- - {({ height }) => ( - - )} - -
- - - - {replaceFileStatus.isError && ( - -
{JSON.stringify(replaceFileStatus.error)}
-
- )} -
- )} + + + + {replaceFileStatus.isError && ( + +
{JSON.stringify(replaceFileStatus.error)}
+
+ )} +
); diff --git a/public/app/features/provisioning/JobStatus.tsx b/public/app/features/provisioning/JobStatus.tsx index 3a9f79123e2..fc970216170 100644 --- a/public/app/features/provisioning/JobStatus.tsx +++ b/public/app/features/provisioning/JobStatus.tsx @@ -29,7 +29,7 @@ export function JobStatus({ name, onStatusChange }: JobStatusProps) { return ( - Loading migration status... + Starting migration... ); } diff --git a/public/app/features/provisioning/OnboardingPage.tsx b/public/app/features/provisioning/OnboardingPage.tsx index b9cedcd3cbc..772b7ef0059 100644 --- a/public/app/features/provisioning/OnboardingPage.tsx +++ b/public/app/features/provisioning/OnboardingPage.tsx @@ -1,12 +1,16 @@ import SVG from 'react-inlinesvg'; +import { useNavigate } from 'react-router-dom-v5-compat'; -import { EmptyState, LinkButton, Alert, Stack, Text } from '@grafana/ui'; +import { EmptyState, LinkButton, Alert, Stack, Text, Button } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { SetupWarnings } from './SetupWarnings'; +import { useGetFrontendSettingsQuery } from './api'; import { NEW_URL, MIGRATE_URL } from './constants'; export default function OnboardingPage({ legacyStorage }: { legacyStorage?: boolean }) { + const settingsQuery = useGetFrontendSettingsQuery(); + const navigate = useNavigate(); return ( } button={ - + } > diff --git a/public/app/features/provisioning/RepositoryListPage.tsx b/public/app/features/provisioning/RepositoryListPage.tsx index 715b9977cc3..5bcb69b6e10 100644 --- a/public/app/features/provisioning/RepositoryListPage.tsx +++ b/public/app/features/provisioning/RepositoryListPage.tsx @@ -62,7 +62,9 @@ export default function RepositoryListPage() { title="Legacy storage detected" severity="error" buttonContent={<>Remove all configured repositories} - onRemove={() => setShowDeleteModal(true)} + onRemove={() => { + setShowDeleteModal(true); + }} > Configured repositories will not work while running legacy storage. diff --git a/public/app/features/provisioning/Wizard/MigrateToProvisioningPage.tsx b/public/app/features/provisioning/Wizard/MigrateToProvisioningPage.tsx index aec72a369d4..3b96e946a21 100644 --- a/public/app/features/provisioning/Wizard/MigrateToProvisioningPage.tsx +++ b/public/app/features/provisioning/Wizard/MigrateToProvisioningPage.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom-v5-compat'; import { Page } from 'app/core/components/Page/Page'; @@ -10,20 +10,26 @@ import { PROVISIONING_URL } from '../constants'; import { ProvisioningWizard } from './ProvisioningWizard'; export default function MigrateToProvisioningPage() { - const settings = useGetFrontendSettingsQuery(); + const settingsQuery = useGetFrontendSettingsQuery(); const navigate = useNavigate(); + const configChecked = useRef(false); + useEffect(() => { - if (settings.data) { - // Do not run the migration wizard if you are already using unified storage - if (!Boolean(settings.data.legacyStorage)) { - navigate(PROVISIONING_URL); - } - // Do not run the migration wizard if something is already targeting the instance - if (settings.data.items.find((v) => v.target === 'instance')) { - navigate(PROVISIONING_URL); + // Only run the redirect logic on the initial success load, not on subsequent data updates + if (settingsQuery.isSuccess && !configChecked.current) { + configChecked.current = true; + if (settingsQuery.data) { + // Do not run the migration wizard if you are already using unified storage + if (!Boolean(settingsQuery.data.legacyStorage)) { + navigate(PROVISIONING_URL); + } + // Do not run the migration wizard if something is already targeting the instance + if (settingsQuery.data.items.find((v) => v.target === 'instance')) { + navigate(PROVISIONING_URL); + } } } - }, [settings.data, navigate]); + }, [settingsQuery.data, navigate, settingsQuery.isSuccess]); return ( ('connection'); const [completedSteps, setCompletedSteps] = useState([]); const [stepSuccess, setStepSuccess] = useState(false); + const settingsQuery = useGetFrontendSettingsQuery(); + const navigate = useNavigate(); const methods = useForm({ defaultValues: { repository: getDefaultValues(), @@ -60,6 +62,7 @@ export function ProvisioningWizard() { setActiveStep(steps[currentStepIndex + 1].id); setStepSuccess(false); } else if (activeStep === 'migrate') { + settingsQuery.refetch(); navigate('/dashboards'); } }; diff --git a/public/app/features/provisioning/api/endpoints.gen.ts b/public/app/features/provisioning/api/endpoints.gen.ts index 3111e32450f..5c8d789fbd2 100644 --- a/public/app/features/provisioning/api/endpoints.gen.ts +++ b/public/app/features/provisioning/api/endpoints.gen.ts @@ -263,11 +263,11 @@ const injectedRtkApi = api }), getFrontendSettings: build.query({ query: () => ({ url: `/settings` }), - providesTags: ['Provisioning'], + providesTags: ['Provisioning', 'Repository'], }), getResourceStats: build.query({ query: () => ({ url: `/stats` }), - providesTags: ['Provisioning'], + providesTags: ['Provisioning', 'Repository'], }), }), overrideExisting: false, diff --git a/public/app/features/provisioning/hooks.ts b/public/app/features/provisioning/hooks.ts deleted file mode 100644 index ae052bfe41b..00000000000 --- a/public/app/features/provisioning/hooks.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { skipToken } from '@reduxjs/toolkit/query/react'; -import { useCallback } from 'react'; - -import { useUrlParams } from 'app/core/navigation/hooks'; - -import { useGetFolderQuery } from '../browse-dashboards/api/browseDashboardsAPI'; - -import { - ListRepositoryArg, - ReplaceRepositoryFilesWithPathArg, - Repository, - RepositorySpec, - useCreateRepositoryFilesWithPathMutation, - useCreateRepositoryMutation, - useListRepositoryQuery, - useReplaceRepositoryFilesWithPathMutation, - useReplaceRepositoryMutation, - useListJobQuery, - Job, -} from './api'; - -export function useCreateOrUpdateRepository(name?: string) { - const [create, createRequest] = useCreateRepositoryMutation(); - const [update, updateRequest] = useReplaceRepositoryMutation(); - - const generateRepositoryMetadata = (data: RepositorySpec) => { - // We don't know for sure that we can use a normalised name. If we can't, we'll ask the server to generate one for us. - const normalisedName = data.title.toLowerCase().replaceAll(/[^a-z0-9\-_]+/g, ''); - - if ( - crypto.randomUUID && // we might not be in a secure context - normalisedName && // we need a non-empty string before we check the first character - normalisedName.charAt(0) >= 'a' && // required to start with a letter to be a valid k8s name - normalisedName.charAt(0) <= 'z' && - normalisedName.replaceAll(/[^a-z]/g, '').length >= 3 // must look sensible to a human - ) { - // We still want a suffix, to avoid name collisions. - const randomBit = crypto.randomUUID().substring(0, 7); - const shortenedName = normalisedName.substring(0, 63 - 1 - randomBit.length); - return { name: `${shortenedName}-${randomBit}` }; - } else { - return { generateName: 'r' }; - } - }; - - const updateOrCreate = useCallback( - (data: RepositorySpec) => { - if (name) { - return update({ name, repository: { metadata: { name }, spec: data } }); - } - return create({ repository: { metadata: generateRepositoryMetadata(data), spec: data } }); - }, - [create, name, update] - ); - - return [updateOrCreate, name ? updateRequest : createRequest] as const; -} - -// Sort repositories alphabetically by title -export function useRepositoryList(options: ListRepositoryArg = {}): [Repository[] | undefined, boolean] { - const query = useListRepositoryQuery(options); - const collator = new Intl.Collator(undefined, { numeric: true }); - - const sortedItems = query.data?.items?.slice().sort((a, b) => { - const titleA = a.spec?.title ?? ''; - const titleB = b.spec?.title ?? ''; - return collator.compare(titleA, titleB); - }); - - return [sortedItems, query.isLoading]; -} - -export function useCreateOrUpdateRepositoryFile(name?: string) { - const [create, createRequest] = useCreateRepositoryFilesWithPathMutation(); - const [update, updateRequest] = useReplaceRepositoryFilesWithPathMutation(); - - const updateOrCreate = useCallback( - (data: ReplaceRepositoryFilesWithPathArg) => { - const actions = name ? update : create; - return actions(data); - }, - [create, name, update] - ); - - return [updateOrCreate, name ? updateRequest : createRequest] as const; -} - -export const usePullRequestParam = () => { - const [params] = useUrlParams(); - const prParam = params.get('pull_request_url'); - - if (!prParam) { - return undefined; - } - - return decodeURIComponent(prParam); -}; - -interface GetResourceRepositoryArgs { - name?: string; - folderUid?: string; -} - -export const useGetResourceRepository = ({ name, folderUid }: GetResourceRepositoryArgs) => { - const [items, isLoading] = useRepositoryList(); - // Get the folder data from API to get repository data for nested folders - const folderQuery = useGetFolderQuery(name || !folderUid ? skipToken : folderUid); - - const repoName = name || folderQuery.data?.repository?.name; - - if (!items?.length || isLoading || !repoName) { - return undefined; - } - - return items.find((repo) => repo.metadata?.name === repoName); -}; - -interface RepositoryJobsArgs { - name?: string; - watch?: boolean; -} - -export function useRepositoryJobs({ name, watch = true }: RepositoryJobsArgs = {}): [ - Job[] | undefined, - ReturnType, -] { - const query = useListJobQuery( - name - ? { - labelSelector: `repository=${name}`, - watch, - } - : skipToken - ); - - const collator = new Intl.Collator(undefined, { numeric: true }); - - const sortedItems = query.data?.items?.slice().sort((a, b) => { - const aTime = a.metadata?.creationTimestamp ?? ''; - const bTime = b.metadata?.creationTimestamp ?? ''; - return collator.compare(bTime, aTime); // Reverse order for newest first - }); - - return [sortedItems, query]; -} diff --git a/public/app/features/provisioning/hooks/index.ts b/public/app/features/provisioning/hooks/index.ts new file mode 100644 index 00000000000..caa4a80549a --- /dev/null +++ b/public/app/features/provisioning/hooks/index.ts @@ -0,0 +1,6 @@ +export { useCreateOrUpdateRepository } from './useCreateOrUpdateRepository'; +export { useCreateOrUpdateRepositoryFile } from './useCreateOrUpdateRepositoryFile'; +export { useGetResourceRepository } from './useGetResourceRepository'; +export { usePullRequestParam } from './usePullRequestParam'; +export { useRepositoryJobs } from './useRepositoryJobs'; +export { useRepositoryList } from './useRepositoryList'; diff --git a/public/app/features/provisioning/hooks/useCreateOrUpdateRepository.ts b/public/app/features/provisioning/hooks/useCreateOrUpdateRepository.ts new file mode 100644 index 00000000000..41f79682b18 --- /dev/null +++ b/public/app/features/provisioning/hooks/useCreateOrUpdateRepository.ts @@ -0,0 +1,40 @@ +import { useCallback } from 'react'; + +import { RepositorySpec, useCreateRepositoryMutation, useReplaceRepositoryMutation } from '../api'; + +export function useCreateOrUpdateRepository(name?: string) { + const [create, createRequest] = useCreateRepositoryMutation(); + const [update, updateRequest] = useReplaceRepositoryMutation(); + + const generateRepositoryMetadata = (data: RepositorySpec) => { + // We don't know for sure that we can use a normalised name. If we can't, we'll ask the server to generate one for us. + const normalisedName = data.title.toLowerCase().replaceAll(/[^a-z0-9\-_]+/g, ''); + + if ( + crypto.randomUUID && // we might not be in a secure context + normalisedName && // we need a non-empty string before we check the first character + normalisedName.charAt(0) >= 'a' && // required to start with a letter to be a valid k8s name + normalisedName.charAt(0) <= 'z' && + normalisedName.replaceAll(/[^a-z]/g, '').length >= 3 // must look sensible to a human + ) { + // We still want a suffix, to avoid name collisions. + const randomBit = crypto.randomUUID().substring(0, 7); + const shortenedName = normalisedName.substring(0, 63 - 1 - randomBit.length); + return { name: `${shortenedName}-${randomBit}` }; + } else { + return { generateName: 'r' }; + } + }; + + const updateOrCreate = useCallback( + (data: RepositorySpec) => { + if (name) { + return update({ name, repository: { metadata: { name }, spec: data } }); + } + return create({ repository: { metadata: generateRepositoryMetadata(data), spec: data } }); + }, + [create, name, update] + ); + + return [updateOrCreate, name ? updateRequest : createRequest] as const; +} diff --git a/public/app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile.ts b/public/app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile.ts new file mode 100644 index 00000000000..dbcc9ffb978 --- /dev/null +++ b/public/app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile.ts @@ -0,0 +1,22 @@ +import { useCallback } from 'react'; + +import { + ReplaceRepositoryFilesWithPathArg, + useCreateRepositoryFilesWithPathMutation, + useReplaceRepositoryFilesWithPathMutation, +} from '../api'; + +export function useCreateOrUpdateRepositoryFile(name?: string) { + const [create, createRequest] = useCreateRepositoryFilesWithPathMutation(); + const [update, updateRequest] = useReplaceRepositoryFilesWithPathMutation(); + + const updateOrCreate = useCallback( + (data: ReplaceRepositoryFilesWithPathArg) => { + const actions = name ? update : create; + return actions(data); + }, + [create, name, update] + ); + + return [updateOrCreate, name ? updateRequest : createRequest] as const; +} diff --git a/public/app/features/provisioning/hooks/useGetResourceRepository.ts b/public/app/features/provisioning/hooks/useGetResourceRepository.ts new file mode 100644 index 00000000000..90ca6e82cb1 --- /dev/null +++ b/public/app/features/provisioning/hooks/useGetResourceRepository.ts @@ -0,0 +1,24 @@ +import { skipToken } from '@reduxjs/toolkit/query/react'; + +import { useGetFolderQuery } from '../../browse-dashboards/api/browseDashboardsAPI'; + +import { useRepositoryList } from './useRepositoryList'; + +interface GetResourceRepositoryArgs { + name?: string; + folderUid?: string; +} + +export const useGetResourceRepository = ({ name, folderUid }: GetResourceRepositoryArgs) => { + const [items, isLoading] = useRepositoryList(); + // Get the folder data from API to get repository data for nested folders + const folderQuery = useGetFolderQuery(name || !folderUid ? skipToken : folderUid); + + const repoName = name || folderQuery.data?.repository?.name; + + if (!items?.length || isLoading || !repoName) { + return undefined; + } + + return items.find((repo) => repo.metadata?.name === repoName); +}; diff --git a/public/app/features/provisioning/hooks/usePullRequestParam.ts b/public/app/features/provisioning/hooks/usePullRequestParam.ts new file mode 100644 index 00000000000..2aefe258ddc --- /dev/null +++ b/public/app/features/provisioning/hooks/usePullRequestParam.ts @@ -0,0 +1,12 @@ +import { useUrlParams } from 'app/core/navigation/hooks'; + +export const usePullRequestParam = () => { + const [params] = useUrlParams(); + const prParam = params.get('pull_request_url'); + + if (!prParam) { + return undefined; + } + + return decodeURIComponent(prParam); +}; diff --git a/public/app/features/provisioning/hooks/useRepositoryJobs.ts b/public/app/features/provisioning/hooks/useRepositoryJobs.ts new file mode 100644 index 00000000000..1935cf167ef --- /dev/null +++ b/public/app/features/provisioning/hooks/useRepositoryJobs.ts @@ -0,0 +1,32 @@ +import { skipToken } from '@reduxjs/toolkit/query/react'; + +import { Job, useListJobQuery } from '../api'; + +interface RepositoryJobsArgs { + name?: string; + watch?: boolean; +} + +export function useRepositoryJobs({ name, watch = true }: RepositoryJobsArgs = {}): [ + Job[] | undefined, + ReturnType, +] { + const query = useListJobQuery( + name + ? { + labelSelector: `repository=${name}`, + watch, + } + : skipToken + ); + + const collator = new Intl.Collator(undefined, { numeric: true }); + + const sortedItems = query.data?.items?.slice().sort((a, b) => { + const aTime = a.metadata?.creationTimestamp ?? ''; + const bTime = b.metadata?.creationTimestamp ?? ''; + return collator.compare(bTime, aTime); // Reverse order for newest first + }); + + return [sortedItems, query]; +} diff --git a/public/app/features/provisioning/hooks/useRepositoryList.ts b/public/app/features/provisioning/hooks/useRepositoryList.ts new file mode 100644 index 00000000000..d9a339c1e18 --- /dev/null +++ b/public/app/features/provisioning/hooks/useRepositoryList.ts @@ -0,0 +1,15 @@ +import { ListRepositoryArg, Repository, useListRepositoryQuery } from '../api'; + +// Sort repositories alphabetically by title +export function useRepositoryList(options: ListRepositoryArg = {}): [Repository[] | undefined, boolean] { + const query = useListRepositoryQuery(options); + const collator = new Intl.Collator(undefined, { numeric: true }); + + const sortedItems = query.data?.items?.slice().sort((a, b) => { + const titleA = a.spec?.title ?? ''; + const titleB = b.spec?.title ?? ''; + return collator.compare(titleA, titleB); + }); + + return [sortedItems, query.isLoading]; +}