POC/Provisioning: Save dashboard (#97167)
* Create provisioned db * Redirect to preview * Enable import for all * Add "Provisioned" badge * Add "Provisioned" badge * Do not show save for provisioned db * Show existing PRs * Betterer
This commit is contained in:
1 parent
2972570075
commit
366121b355
8 files changed
+171
-28
No files matched your search
+2
-1
@@ -2074,7 +2074,8 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
|
||||
[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 />", "8"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/saving/SaveProvisionedDashboardForm.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
|
||||
@@ -19,6 +19,7 @@ interface SaveDashboardDrawerState extends SceneObjectState {
|
||||
saveRefresh?: boolean;
|
||||
saveAsCopy?: boolean;
|
||||
onSaveSuccess?: () => void;
|
||||
saveProvisioned?: boolean;
|
||||
}
|
||||
|
||||
export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerState> {
|
||||
@@ -39,7 +40,7 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
|
||||
};
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<SaveDashboardDrawer>) => {
|
||||
const { showDiff, saveAsCopy, saveTimeRange, saveVariables, saveRefresh } = model.useState();
|
||||
const { saveProvisioned, showDiff, saveAsCopy, saveTimeRange, saveVariables, saveRefresh } = model.useState();
|
||||
const changeInfo = getDashboardChangesFromScene(
|
||||
model.state.dashboardRef.resolve(),
|
||||
saveTimeRange,
|
||||
@@ -51,9 +52,8 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
|
||||
const dashboard = model.state.dashboardRef.resolve();
|
||||
const { meta } = dashboard.useState();
|
||||
const { provisioned: isProvisioned, folderTitle } = meta;
|
||||
|
||||
// Provisioned dashboards have k8s metadata annotations
|
||||
const isProvisionedNG = meta.k8s?.annotations?.[AnnoKeyRepoName];
|
||||
const isProvisionedNG = saveProvisioned || meta.k8s?.annotations?.[AnnoKeyRepoName];
|
||||
|
||||
const tabs = (
|
||||
<TabsBar>
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents } from '@grafana/runtime';
|
||||
import { Alert, Button, Field, Input, LinkButton, RadioButtonGroup, Stack, TextArea } from '@grafana/ui';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Field,
|
||||
Input,
|
||||
LinkButton,
|
||||
RadioButtonGroup,
|
||||
Spinner,
|
||||
Stack,
|
||||
TextArea,
|
||||
TextLink,
|
||||
} from '@grafana/ui';
|
||||
import { AnnoKeyRepoName, AnnoKeyRepoPath } from 'app/features/apiserver/types';
|
||||
import { DashboardMeta } from 'app/types';
|
||||
|
||||
import { useGetRepositoryQuery, useUpdateRepositoryFilesMutation } from '../../provisioning/api';
|
||||
import { RepositorySelect } from '../../provisioning/RepositorySelect';
|
||||
import { useGetRepositoryQuery } from '../../provisioning/api';
|
||||
import { RepositorySpec } from '../../provisioning/api/types';
|
||||
import { PROVISIONING_URL } from '../../provisioning/constants';
|
||||
import { useCreateOrUpdateRepositoryFile, usePullRequestParam } from '../../provisioning/hooks';
|
||||
import { WorkflowOption } from '../../provisioning/types';
|
||||
import { createPRLink, validateBranchName } from '../../provisioning/utils/git';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
@@ -27,7 +43,7 @@ type FormData = {
|
||||
function getDefaultValues(meta: DashboardMeta) {
|
||||
const anno = meta.k8s?.annotations;
|
||||
let ref = '';
|
||||
let path = anno?.[AnnoKeyRepoPath] ?? '';
|
||||
let path = anno?.[AnnoKeyRepoPath] ?? `${meta.slug}.json` ?? '';
|
||||
const repo = anno?.[AnnoKeyRepoName] ?? '';
|
||||
const idx = path.indexOf('#');
|
||||
if (idx > 0) {
|
||||
@@ -42,6 +58,10 @@ function getDefaultValues(meta: DashboardMeta) {
|
||||
};
|
||||
}
|
||||
|
||||
const getDefaultWorkflow = (config?: RepositorySpec) => {
|
||||
return config?.github?.branchWorkflow ? WorkflowOption.Branch : WorkflowOption.PullRequest;
|
||||
};
|
||||
|
||||
function getWorkflowOptions(branch = 'main') {
|
||||
return [
|
||||
{ label: `Commit to ${branch}`, value: WorkflowOption.Branch },
|
||||
@@ -57,26 +77,31 @@ export interface Props {
|
||||
}
|
||||
|
||||
export function SaveProvisionedDashboard({ meta, drawer, changeInfo, dashboard }: Props) {
|
||||
const [saveDashboard, request] = useUpdateRepositoryFilesMutation();
|
||||
// Saving as a new provisioned dashboard
|
||||
const { saveProvisioned } = drawer.useState();
|
||||
const prURL = usePullRequestParam();
|
||||
const defaultValues = getDefaultValues(meta);
|
||||
const repositoryConfigQuery = useGetRepositoryQuery({ name: defaultValues.repo });
|
||||
const repositoryConfig = repositoryConfigQuery?.data?.spec;
|
||||
const isGitHub = repositoryConfig?.type === 'github';
|
||||
const [action, request] = useCreateOrUpdateRepositoryFile(saveProvisioned ? undefined : defaultValues.path);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { errors },
|
||||
control,
|
||||
setValue,
|
||||
} = useForm({
|
||||
defaultValues: {
|
||||
...defaultValues,
|
||||
workflow: repositoryConfig?.github?.branchWorkflow ? WorkflowOption.Branch : WorkflowOption.PullRequest,
|
||||
workflow: WorkflowOption.PullRequest,
|
||||
},
|
||||
});
|
||||
const [repo, ref, workflow] = watch(['repo', 'ref', 'workflow']);
|
||||
const [repo, ref, workflow, path] = watch(['repo', 'ref', 'workflow', 'path']);
|
||||
const repositoryConfigQuery = useGetRepositoryQuery({ name: repo });
|
||||
const repositoryConfig = repositoryConfigQuery?.data?.spec;
|
||||
const isGitHub = repositoryConfig?.type === 'github';
|
||||
const href = createPRLink(repositoryConfig, repo, ref);
|
||||
const { isDirty } = dashboard.state;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const appEvents = getAppEvents();
|
||||
@@ -86,13 +111,21 @@ export function SaveProvisionedDashboard({ meta, drawer, changeInfo, dashboard }
|
||||
payload: ['Dashboard saved'],
|
||||
});
|
||||
dashboard.setState({ isDirty: false });
|
||||
|
||||
if (saveProvisioned) {
|
||||
navigate(`${PROVISIONING_URL}/${repo}/dashboard/preview/${path}?ref=${ref}`);
|
||||
}
|
||||
} else if (request.isError) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertError.name,
|
||||
payload: ['Error saving dashboard', request.error],
|
||||
});
|
||||
}
|
||||
}, [request.isSuccess, request.isError, request.error, dashboard]);
|
||||
}, [request.isSuccess, request.isError, request.error, dashboard, path, saveProvisioned, navigate, repo, ref]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue('workflow', getDefaultWorkflow(repositoryConfig));
|
||||
}, [repositoryConfig, setValue]);
|
||||
|
||||
const doSave = ({ ref, path, comment, repo }: FormData) => {
|
||||
if (!repo || !path) {
|
||||
@@ -102,9 +135,16 @@ export function SaveProvisionedDashboard({ meta, drawer, changeInfo, dashboard }
|
||||
if (workflow === WorkflowOption.Branch) {
|
||||
ref = repositoryConfig?.github?.branch || 'main';
|
||||
}
|
||||
saveDashboard({ ref, name: repo, path, message: comment, body: changeInfo.changedSaveModel });
|
||||
action({ ref, name: repo, path, message: comment, body: changeInfo.changedSaveModel });
|
||||
};
|
||||
|
||||
if (repositoryConfigQuery.isLoading) {
|
||||
return (
|
||||
<Stack justifyContent={'center'}>
|
||||
<Spinner />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<form onSubmit={handleSubmit(doSave)}>
|
||||
<Stack direction="column" gap={2}>
|
||||
@@ -117,11 +157,21 @@ export function SaveProvisionedDashboard({ meta, drawer, changeInfo, dashboard }
|
||||
<SaveDashboardFormCommonOptions drawer={drawer} changeInfo={changeInfo} />
|
||||
|
||||
<Field label="Repository">
|
||||
<Input {...register('repo')} readOnly />
|
||||
{saveProvisioned ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name={'repo'}
|
||||
render={({ field: { ref, onChange, ...field } }) => {
|
||||
return <RepositorySelect {...field} onChange={(v) => onChange(v.value)} />;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Input {...register('repo')} readOnly />
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Field label="Path" description="File path inside the repository. This must be .json or .yaml">
|
||||
<Input {...register('path')} readOnly />
|
||||
<Input {...register('path')} readOnly={!saveProvisioned} />
|
||||
</Field>
|
||||
|
||||
{isGitHub && (
|
||||
@@ -130,9 +180,9 @@ export function SaveProvisionedDashboard({ meta, drawer, changeInfo, dashboard }
|
||||
<Controller
|
||||
control={control}
|
||||
name={'workflow'}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<RadioButtonGroup {...field} options={getWorkflowOptions(repositoryConfig.github?.branch)} />
|
||||
)}
|
||||
render={({ field: { ref, ...field } }) => {
|
||||
return <RadioButtonGroup {...field} options={getWorkflowOptions(repositoryConfig.github?.branch)} />;
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
{workflow === WorkflowOption.PullRequest && (
|
||||
@@ -164,8 +214,16 @@ export function SaveProvisionedDashboard({ meta, drawer, changeInfo, dashboard }
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{prURL && (
|
||||
<Alert severity="info" title="Pull request created">
|
||||
A pull request has been created with changes to this dashboard:{' '}
|
||||
<TextLink href={prURL} external>
|
||||
{prURL}
|
||||
</TextLink>
|
||||
</Alert>
|
||||
)}
|
||||
<Stack gap={2}>
|
||||
<Button variant="primary" type="submit" disabled={request.isLoading || !isDirty}>
|
||||
<Button variant="primary" type="submit" disabled={(request.isLoading || !isDirty) && !saveProvisioned}>
|
||||
{request.isLoading ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={drawer.onClose} fill="outline">
|
||||
|
||||
@@ -384,7 +384,15 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
return true;
|
||||
};
|
||||
|
||||
public openSaveDrawer({ saveAsCopy, onSaveSuccess }: { saveAsCopy?: boolean; onSaveSuccess?: () => void }) {
|
||||
public openSaveDrawer({
|
||||
saveAsCopy,
|
||||
saveProvisioned,
|
||||
onSaveSuccess,
|
||||
}: {
|
||||
saveAsCopy?: boolean;
|
||||
saveProvisioned?: boolean;
|
||||
onSaveSuccess?: () => void;
|
||||
}) {
|
||||
if (!this.state.isEditing) {
|
||||
return;
|
||||
}
|
||||
@@ -394,6 +402,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
dashboardRef: this.getRef(),
|
||||
saveAsCopy,
|
||||
onSaveSuccess,
|
||||
saveProvisioned,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
import { ScopesSelector } from 'app/features/scopes';
|
||||
|
||||
import { AnnoKeyRepoName } from '../../apiserver/types';
|
||||
import { shareDashboardType } from '../../dashboard/components/ShareModal/utils';
|
||||
import { PanelEditor, buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import ExportButton from '../sharing/ExportButton/ExportButton';
|
||||
@@ -118,6 +119,16 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
});
|
||||
}
|
||||
|
||||
if (meta.k8s?.annotations?.[AnnoKeyRepoName]) {
|
||||
toolbarActions.push({
|
||||
group: 'icon-actions',
|
||||
condition: true,
|
||||
render: () => {
|
||||
return <Badge color="green" text="Provisioned" key="provisioned-dashboard-button-badge" />;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const isDevEnv = config.buildInfo.env === 'development';
|
||||
|
||||
toolbarActions.push({
|
||||
@@ -548,6 +559,15 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
dashboard.openSaveDrawer({ saveAsCopy: true });
|
||||
}}
|
||||
/>
|
||||
{!meta.k8s?.annotations?.[AnnoKeyRepoName] && (
|
||||
<Menu.Item
|
||||
label="Save to repository"
|
||||
icon="github"
|
||||
onClick={() => {
|
||||
dashboard.openSaveDrawer({ saveProvisioned: true });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
|
||||
@@ -58,10 +58,6 @@ export function ImportFromRepository({ repository }: Props) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
if (repository.spec?.type !== 'github') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant={'secondary'} onClick={() => setIsModalOpen(true)} disabled={importQuery.isLoading || !name}>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Combobox, Spinner } from '@grafana/ui';
|
||||
import { ComboboxBaseProps } from '@grafana/ui/src/components/Combobox/Combobox';
|
||||
|
||||
import { useListRepositoryQuery } from './api';
|
||||
|
||||
export function RepositorySelect(props: Pick<ComboboxBaseProps<string>, 'value' | 'onChange'>) {
|
||||
const listQuery = useListRepositoryQuery();
|
||||
|
||||
if (listQuery.isLoading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
{...props}
|
||||
placeholder={'Select a repository'}
|
||||
options={
|
||||
listQuery.data?.items?.map((repo) => ({
|
||||
label: repo.spec.title,
|
||||
value: repo.metadata.name,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useCreateRepositoryMutation, useListRepositoryQuery, useUpdateRepositoryMutation } from './api';
|
||||
import { RepositoryResource, RepositorySpec } from './api/types';
|
||||
import { useUrlParams } from 'app/core/navigation/hooks';
|
||||
|
||||
import {
|
||||
useCreateRepositoryFilesMutation,
|
||||
useCreateRepositoryMutation,
|
||||
useListRepositoryQuery,
|
||||
useUpdateRepositoryFilesMutation,
|
||||
useUpdateRepositoryMutation,
|
||||
} from './api';
|
||||
import { FileOperationArg, RepositoryResource, RepositorySpec } from './api/types';
|
||||
|
||||
export function useCreateOrUpdateRepository(name?: string) {
|
||||
const [create, createRequest] = useCreateRepositoryMutation();
|
||||
@@ -30,3 +38,29 @@ export function useRepositoryList(): [RepositoryResource[] | undefined, boolean]
|
||||
|
||||
return [sortedItems, query.isLoading];
|
||||
}
|
||||
|
||||
export function useCreateOrUpdateRepositoryFile(name?: string) {
|
||||
const [create, createRequest] = useCreateRepositoryFilesMutation();
|
||||
const [update, updateRequest] = useUpdateRepositoryFilesMutation();
|
||||
|
||||
const updateOrCreate = useCallback(
|
||||
(data: FileOperationArg) => {
|
||||
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);
|
||||
};
|
||||
Reference in new issue
Block a user