mirror of https://github.com/grafana/grafana
Provisioning: Add hooks and utils (#101999)
* Provisioning: Add hooks and utils * Fix folder API call * Update public/app/features/provisioning/utils/git.ts Co-authored-by: Ryan McKinley <ryantxu@gmail.com> * Fixes * Remove unused import --------- Co-authored-by: Ryan McKinley <ryantxu@gmail.com>pull/101980/head
parent
abe6a3121c
commit
dacde69ffb
@ -0,0 +1,4 @@ |
||||
export const PROVISIONING_URL = '/admin/provisioning'; |
||||
export const CONNECT_URL = `${PROVISIONING_URL}/connect`; |
||||
export const MIGRATE_URL = `${PROVISIONING_URL}/migrate`; |
||||
export const GETTING_STARTED_URL = `${PROVISIONING_URL}/getting-started`; |
@ -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 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; |
||||
} |
||||
|
||||
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' }; |
||||
} |
||||
}; |
@ -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,25 @@ |
||||
import { skipToken } from '@reduxjs/toolkit/query/react'; |
||||
|
||||
import { AnnoKeyManagerKind } from '../../apiserver/types'; |
||||
import { useGetFolderQuery } from '../../folders/api'; |
||||
|
||||
import { useRepositoryList } from './useRepositoryList'; |
||||
|
||||
interface GetResourceRepositoryArgs { |
||||
name?: string; |
||||
folderUid?: string; |
||||
} |
||||
|
||||
export const useGetResourceRepository = ({ name, folderUid }: GetResourceRepositoryArgs) => { |
||||
const [items, isLoading] = useRepositoryList(name || !folderUid ? skipToken : undefined); |
||||
// Get the folder data from API to get the repository data for nested folders
|
||||
const folderQuery = useGetFolderQuery(name || !folderUid ? skipToken : { name: folderUid }); |
||||
|
||||
const repoName = name || folderQuery.data?.metadata?.annotations?.[AnnoKeyManagerKind]; |
||||
|
||||
if (!items?.length || isLoading || !repoName) { |
||||
return undefined; |
||||
} |
||||
|
||||
return items.find((repo) => repo.metadata?.name === repoName); |
||||
}; |
@ -0,0 +1,10 @@ |
||||
import { skipToken } from '@reduxjs/toolkit/query'; |
||||
|
||||
import { RepositoryViewList, useGetFrontendSettingsQuery } from '../api'; |
||||
import { checkSyncSettings } from '../utils/checkSyncSettings'; |
||||
|
||||
export function useIsProvisionedInstance(settings?: RepositoryViewList) { |
||||
const settingsQuery = useGetFrontendSettingsQuery(settings ? skipToken : undefined); |
||||
const [instanceConnected] = checkSyncSettings(settings || settingsQuery.data); |
||||
return instanceConnected; |
||||
} |
@ -0,0 +1,20 @@ |
||||
import { useUrlParams } from 'app/core/navigation/hooks'; |
||||
|
||||
import { DashboardScene } from '../../dashboard-scene/scene/DashboardScene'; |
||||
import { useGetFrontendSettingsQuery } from '../api'; |
||||
|
||||
import { useGetResourceRepository } from './useGetResourceRepository'; |
||||
|
||||
export function useIsProvisionedNG(dashboard: DashboardScene): boolean { |
||||
const [params] = useUrlParams(); |
||||
const folderUid = params.get('folderUid') || undefined; |
||||
|
||||
const folderRepository = useGetResourceRepository({ folderUid }); |
||||
const { data } = useGetFrontendSettingsQuery(); |
||||
|
||||
return ( |
||||
dashboard.isManaged() || |
||||
Boolean(folderRepository) || |
||||
Boolean(data?.items.some((item) => item.target === 'instance')) |
||||
); |
||||
} |
@ -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,19 @@ |
||||
import { skipToken } from '@reduxjs/toolkit/query'; |
||||
|
||||
import { ListRepositoryArg, Repository, useListRepositoryQuery } from '../api'; |
||||
|
||||
// Sort repositories alphabetically by title
|
||||
export function useRepositoryList( |
||||
options: ListRepositoryArg | typeof skipToken = {} |
||||
): [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]; |
||||
} |
@ -0,0 +1,14 @@ |
||||
import { GitHubRepositoryConfig, LocalRepositoryConfig, RepositorySpec } from './api'; |
||||
|
||||
export type RepositoryFormData = Omit<RepositorySpec, 'github' | 'local'> & |
||||
GitHubRepositoryConfig & |
||||
LocalRepositoryConfig; |
||||
|
||||
// Added to DashboardDTO to help editor
|
||||
export interface ProvisioningPreview { |
||||
repo: string; |
||||
file: string; |
||||
ref?: string; |
||||
} |
||||
|
||||
export type WorkflowOption = 'branch' | 'write'; |
@ -0,0 +1,10 @@ |
||||
import { RepositoryViewList } from '../api'; |
||||
|
||||
export function checkSyncSettings(settings?: RepositoryViewList): [boolean, boolean] { |
||||
if (!settings?.items?.length) { |
||||
return [false, false]; |
||||
} |
||||
const instanceConnected = settings.items.some((item) => item.target === 'instance'); |
||||
const folderConnected = settings.items.some((item) => item.target === 'folder'); |
||||
return [instanceConnected, folderConnected]; |
||||
} |
@ -0,0 +1,38 @@ |
||||
import { RepositorySpec } from '../api'; |
||||
import { RepositoryFormData } from '../types'; |
||||
|
||||
export const dataToSpec = (data: RepositoryFormData): RepositorySpec => { |
||||
const spec: RepositorySpec = { |
||||
type: data.type, |
||||
sync: data.sync, |
||||
title: data.title || '', |
||||
workflows: data.workflows, |
||||
}; |
||||
switch (data.type) { |
||||
case 'github': |
||||
spec.github = { |
||||
generateDashboardPreviews: data.generateDashboardPreviews, |
||||
url: data.url || '', |
||||
branch: data.branch, |
||||
token: data.token, |
||||
}; |
||||
break; |
||||
case 'local': |
||||
spec.local = { |
||||
path: data.path, |
||||
}; |
||||
break; |
||||
} |
||||
|
||||
return spec; |
||||
}; |
||||
|
||||
export const specToData = (spec: RepositorySpec): RepositoryFormData => { |
||||
return { |
||||
...spec, |
||||
...spec.github, |
||||
...spec.local, |
||||
branch: spec.github?.branch || '', |
||||
url: spec.github?.url || '', |
||||
}; |
||||
}; |
@ -0,0 +1,14 @@ |
||||
/** |
||||
* Validates a Git branch name according to the following rules: |
||||
* 1. The branch name cannot start with `/`, end with `/`, `.`, or whitespace. |
||||
* 2. The branch name cannot contain consecutive slashes (`//`). |
||||
* 3. The branch name cannot contain consecutive dots (`..`). |
||||
* 4. The branch name cannot contain `@{`. |
||||
* 5. The branch name cannot include the following characters: `~`, `^`, `:`, `?`, `*`, `[`, `\`, or `]`.
|
||||
* 6. The branch name must have at least one character and must not be empty. |
||||
*/ |
||||
export function validateBranchName(branchName?: string) { |
||||
const branchNameRegex = /^(?!\/|.*\/\/|.*\.\.|.*@{)(?!.*[~^:?*[\]\\]).+(?<!\/|\.|\s)$/; |
||||
|
||||
return branchName && branchNameRegex.test(branchName!); |
||||
} |
@ -0,0 +1,6 @@ |
||||
export function formatTimestamp(timestamp?: number) { |
||||
if (!timestamp) { |
||||
return 'N/A'; |
||||
} |
||||
return new Date(timestamp).toLocaleString(); |
||||
} |
Loading…
Reference in new issue