The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx

257 lines
8.7 KiB

import { css } from '@emotion/css';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom-v5-compat';
import { AppEvents, GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { getAppEvents } from '@grafana/runtime';
import { Alert, Text, Button, Field, Icon, Input, Stack, useStyles2 } from '@grafana/ui';
import { Folder } from 'app/api/clients/folder/v1beta1';
import { RepositoryView, useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1';
import { AnnoKeySourcePath, Resource } from 'app/features/apiserver/types';
import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields';
import { BaseProvisionedFormData } from 'app/features/dashboard-scene/saving/shared';
import { PROVISIONING_URL } from 'app/features/provisioning/constants';
import { usePullRequestParam } from 'app/features/provisioning/hooks/usePullRequestParam';
import { FolderDTO } from 'app/types/folders';
import { useProvisionedFolderFormData } from '../hooks/useProvisionedFolderFormData';
import { validateFolderName } from './NewFolderForm';
import { formatFolderName, hasFolderNameCharactersToReplace } from './utils';
interface FormProps extends Props {
initialValues: BaseProvisionedFormData;
repository?: RepositoryView;
workflowOptions: Array<{ label: string; value: string }>;
folder?: Folder;
isGitHub: boolean;
}
interface Props {
parentFolder?: FolderDTO;
onDismiss?: () => void;
}
function FormContent({ initialValues, repository, workflowOptions, folder, isGitHub, onDismiss }: FormProps) {
const { prURL } = usePullRequestParam();
const navigate = useNavigate();
const [create, request] = useCreateRepositoryFilesWithPathMutation();
const methods = useForm<BaseProvisionedFormData>({
defaultValues: initialValues,
mode: 'onBlur', // Validates when user leaves the field
});
const { handleSubmit, watch, register, formState } = methods;
const [workflow, ref, title] = watch(['workflow', 'ref', 'title']);
// TODO: replace with useProvisionedRequestHandler hook
useEffect(() => {
const appEvents = getAppEvents();
if (request.isSuccess && repository) {
onDismiss?.();
appEvents.publish({
type: AppEvents.alertSuccess.name,
payload: [
t(
'browse-dashboards.new-provisioned-folder-form.alert-folder-created-successfully',
'Folder created successfully'
),
],
});
// TODO: Update when the upsert type is fixed
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const folder = request.data.resource?.upsert as Resource;
if (folder?.metadata?.name) {
navigate(`/dashboards/f/${folder?.metadata?.name}/`);
return;
}
let url = `${PROVISIONING_URL}/${repository.name}/file/${request.data.path}`;
if (request.data.ref?.length) {
url += '?ref=' + request.data.ref;
}
navigate(url);
} else if (request.isError) {
appEvents.publish({
type: AppEvents.alertError.name,
payload: [
t('browse-dashboards.new-provisioned-folder-form.alert-error-creating-folder', 'Error creating folder'),
request.error,
],
});
}
}, [request.isSuccess, request.isError, request.error, ref, request.data, workflow, navigate, repository, onDismiss]);
const doSave = async ({ ref, title, workflow, comment }: BaseProvisionedFormData) => {
const repoName = repository?.name;
if (!title || !repoName) {
return;
}
const basePath = folder?.metadata?.annotations?.[AnnoKeySourcePath] ?? '';
// Convert folder title to filename format (lowercase, replace spaces with hyphens)
const titleInFilenameFormat = formatFolderName(title); // TODO: this is currently not working, issue created https://github.com/grafana/git-ui-sync-project/issues/314
const prefix = basePath ? `${basePath}/` : '';
const path = `${prefix}${titleInFilenameFormat}/`;
const folderModel = {
title,
type: 'folder',
};
if (workflow === 'write') {
ref = undefined;
}
create({
ref,
name: repoName,
path,
message: comment || `Create folder: ${title}`,
body: folderModel,
});
};
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(doSave)}>
<Stack direction="column" gap={2}>
{!repository?.workflows?.length && (
<Alert
title={t(
'browse-dashboards.new-provisioned-folder-form.title-this-repository-is-read-only',
'This repository is read only'
)}
>
<Trans i18nKey="browse-dashboards.text-this-repository-is-read-only">
If you have direct access to the target, copy the JSON and paste it there.
</Trans>
</Alert>
)}
<Field
noMargin
label={t('browse-dashboards.new-provisioned-folder-form.label-folder-name', 'Folder name')}
invalid={!!formState.errors.title}
error={formState.errors.title?.message}
>
<Input
{...register('title', {
required: t('browse-dashboards.new-provisioned-folder-form.error-required', 'Folder name is required'),
validate: validateFolderName,
})}
placeholder={t(
'browse-dashboards.new-provisioned-folder-form.folder-name-input-placeholder-enter-folder-name',
'Enter folder name'
)}
id="folder-name-input"
/>
</Field>
<FolderNamePreviewMessage folderName={title} />
<ResourceEditFormSharedFields
resourceType="folder"
isNew={false}
workflow={workflow}
workflowOptions={workflowOptions}
isGitHub={isGitHub}
hidePath
/>
{prURL && (
<Alert
severity="info"
title={t(
'browse-dashboards.new-provisioned-folder-form.title-pull-request-created',
'Pull request created'
)}
>
<Trans i18nKey="browse-dashboards.new-provisioned-folder-form.text-pull-request-created">
A pull request has been created with changes to this folder:
</Trans>{' '}
<a href={prURL} target="_blank" rel="noopener noreferrer">
{prURL}
</a>
</Alert>
)}
<Stack gap={2}>
<Button variant="secondary" fill="outline" onClick={onDismiss}>
<Trans i18nKey="browse-dashboards.new-provisioned-folder-form.cancel">Cancel</Trans>
</Button>
<Button type="submit" disabled={request.isLoading}>
{request.isLoading
? t('browse-dashboards.new-provisioned-folder-form.button-creating', 'Creating...')
: t('browse-dashboards.new-provisioned-folder-form.button-create', 'Create')}
</Button>
</Stack>
</Stack>
</form>
</FormProvider>
);
}
export function NewProvisionedFolderForm({ parentFolder, onDismiss }: Props) {
const { workflowOptions, isGitHub, repository, folder, initialValues } = useProvisionedFolderFormData({
folderUid: parentFolder?.uid,
action: 'create',
title: '', // Empty title for new folders
});
if (!initialValues) {
return null;
}
return (
<FormContent
parentFolder={parentFolder}
onDismiss={onDismiss}
initialValues={initialValues}
repository={repository}
workflowOptions={workflowOptions}
folder={folder}
isGitHub={isGitHub}
/>
);
}
function FolderNamePreviewMessage({ folderName }: { folderName: string }) {
const styles = useStyles2(getStyles);
const isValidFolderName =
folderName.length && hasFolderNameCharactersToReplace(folderName) && validateFolderName(folderName);
if (!isValidFolderName) {
return null;
}
return (
<div className={styles.folderNameMessage}>
<Icon name="check-circle" type="solid" />
<Text color="success">
{t(
'browse-dashboards.new-provisioned-folder-form.text-your-folder-will-be-created-as',
'Your folder will be created as {{folderName}}',
{
folderName: formatFolderName(folderName),
}
)}
</Text>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
folderNameMessage: css({
display: 'flex',
alignItems: 'center',
fontSize: theme.typography.bodySmall.fontSize,
color: theme.colors.success.text,
}),
};
};