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 <ryantxu@gmail.com>
This commit is contained in:
1 parent
caf4b3d346
commit
ff87355aba
18 files changed
+248
-232
No files matched your search
+10
-4
@@ -5756,8 +5756,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"]
|
||||
],
|
||||
"public/app/features/provisioning/JobStatus.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "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 <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/provisioning/TokenPermissionsInfo.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> 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"],
|
||||
|
||||
@@ -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{
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
))}
|
||||
</TabsBar>
|
||||
<TabContent>
|
||||
{tab === TabSelection.Lint && (
|
||||
<div>
|
||||
{wrap.lint ? (
|
||||
wrap.lint.map((r, idx) => {
|
||||
return (
|
||||
//@ts-expect-error TODO fix lint response types
|
||||
<Alert title={r.rule} severity={r.severity} key={`${r.rule}/${idx}`}>
|
||||
{r.message}
|
||||
</Alert>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div>
|
||||
<h3>No lint issues</h3>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div style={{ height: 800 }}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<CodeEditor
|
||||
width="100%"
|
||||
height={height}
|
||||
language={'json'}
|
||||
showLineNumbers={true}
|
||||
showMiniMap={true}
|
||||
value={jsonView}
|
||||
onBlur={setJsonView}
|
||||
onSave={setJsonView}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab !== TabSelection.Lint && (
|
||||
<div>
|
||||
<div style={{ height: 800 }}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<CodeEditor
|
||||
width="100%"
|
||||
height={height}
|
||||
language={'json'}
|
||||
showLineNumbers={true}
|
||||
showMiniMap={true}
|
||||
value={jsonView}
|
||||
onBlur={setJsonView}
|
||||
onSave={setJsonView}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<Stack direction={'row'}>
|
||||
<Button
|
||||
disabled={replaceFileStatus.isLoading}
|
||||
onClick={() => {
|
||||
replaceFile({
|
||||
name: repo,
|
||||
path: wrap.path!,
|
||||
body: JSON.parse(jsonView),
|
||||
message: 'updated from repo test UI',
|
||||
});
|
||||
}}
|
||||
>
|
||||
{replaceFileStatus.isLoading ? 'Saving' : 'Save'}
|
||||
</Button>
|
||||
</Stack>
|
||||
{replaceFileStatus.isError && (
|
||||
<Alert title="Error saving file">
|
||||
<pre>{JSON.stringify(replaceFileStatus.error)}</pre>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Stack direction={'row'}>
|
||||
<Button
|
||||
disabled={replaceFileStatus.isLoading}
|
||||
onClick={() => {
|
||||
replaceFile({
|
||||
name: repo,
|
||||
path: wrap.path!,
|
||||
body: JSON.parse(jsonView),
|
||||
message: 'updated from repo test UI',
|
||||
});
|
||||
}}
|
||||
>
|
||||
{replaceFileStatus.isLoading ? 'Saving' : 'Save'}
|
||||
</Button>
|
||||
</Stack>
|
||||
{replaceFileStatus.isError && (
|
||||
<Alert title="Error saving file">
|
||||
<pre>{JSON.stringify(replaceFileStatus.error)}</pre>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</TabContent>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ export function JobStatus({ name, onStatusChange }: JobStatusProps) {
|
||||
return (
|
||||
<Stack direction="column" alignItems="center" gap={2}>
|
||||
<Spinner />
|
||||
<Text>Loading migration status...</Text>
|
||||
<Text>Starting migration...</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Page
|
||||
navId="provisioning"
|
||||
@@ -25,9 +29,16 @@ export default function OnboardingPage({ legacyStorage }: { legacyStorage?: bool
|
||||
message="Set up your provisioning connection!"
|
||||
image={<SVG src="public/img/provisioning-empty.svg" width={300} />}
|
||||
button={
|
||||
<LinkButton size="lg" icon="plus" href={MIGRATE_URL}>
|
||||
<Button
|
||||
size="lg"
|
||||
icon="plus"
|
||||
onClick={async () => {
|
||||
await settingsQuery.refetch();
|
||||
navigate(MIGRATE_URL);
|
||||
}}
|
||||
>
|
||||
Connect Grafana to repository
|
||||
</LinkButton>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Stack direction="column" alignItems="center">
|
||||
|
||||
@@ -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.
|
||||
</Alert>
|
||||
|
||||
@@ -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 (
|
||||
<Page
|
||||
|
||||
@@ -7,6 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { getDefaultValues } from '../ConfigForm';
|
||||
import { useGetFrontendSettingsQuery } from '../api';
|
||||
|
||||
import { ConnectionStep } from './ConnectionStep';
|
||||
import { MigrateStep } from './MigrateStep';
|
||||
@@ -27,10 +28,11 @@ const nextButtonText = {
|
||||
} as const;
|
||||
|
||||
export function ProvisioningWizard() {
|
||||
const navigate = useNavigate();
|
||||
const [activeStep, setActiveStep] = useState<WizardStep>('connection');
|
||||
const [completedSteps, setCompletedSteps] = useState<WizardStep[]>([]);
|
||||
const [stepSuccess, setStepSuccess] = useState(false);
|
||||
const settingsQuery = useGetFrontendSettingsQuery();
|
||||
const navigate = useNavigate();
|
||||
const methods = useForm<WizardFormData>({
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -263,11 +263,11 @@ const injectedRtkApi = api
|
||||
}),
|
||||
getFrontendSettings: build.query<GetFrontendSettingsResponse, GetFrontendSettingsArg>({
|
||||
query: () => ({ url: `/settings` }),
|
||||
providesTags: ['Provisioning'],
|
||||
providesTags: ['Provisioning', 'Repository'],
|
||||
}),
|
||||
getResourceStats: build.query<GetResourceStatsResponse, GetResourceStatsArg>({
|
||||
query: () => ({ url: `/stats` }),
|
||||
providesTags: ['Provisioning'],
|
||||
providesTags: ['Provisioning', 'Repository'],
|
||||
}),
|
||||
}),
|
||||
overrideExisting: false,
|
||||
|
||||
@@ -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<typeof useListJobQuery>,
|
||||
] {
|
||||
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];
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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<typeof useListJobQuery>,
|
||||
] {
|
||||
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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
Reference in new issue
Block a user