POC/GitSync: Update dashboard save (#96982)

* Cleanup

* standalone

* move the save UI to its own file

* Use API hooks

* Error/success handling

* Use form

* Cleanup

* Clean dashboard state

* Update endpoints

* Use updated endpoints

* Codeformat

* Betterer

---------

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Alex KhomenkoandRyan McKinley authored and GitHub committed 2024-11-25 15:10:51 +02:00
1 parent d4c5171f4a
commit a30f540720
6 files changed
+102 -129

No files matched your search

+5 -8
View File
@@ -2071,11 +2071,6 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
],
"public/app/features/dashboard-scene/saving/SaveDashboardDrawer.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
],
"public/app/features/dashboard-scene/saving/SaveDashboardForm.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
@@ -2084,6 +2079,11 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
],
"public/app/features/dashboard-scene/saving/SaveProvisionedDashboard.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
],
"public/app/features/dashboard-scene/saving/SaveProvisionedDashboardForm.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
@@ -3707,9 +3707,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/features/provisioning/dashboard.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"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"],
@@ -53,7 +53,7 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
const { provisioned: isProvisioned, folderTitle } = meta;
// Provisioned dashboards have k8s metadata annotations
const isProvisionedNG = meta.k8s?.annotations?.[AnnoKeyRepoName]
const isProvisionedNG = meta.k8s?.annotations?.[AnnoKeyRepoName];
const tabs = (
<TabsBar>
@@ -1,17 +1,39 @@
import { css } from '@emotion/css';
import { useEffect, useMemo, useState } from 'react';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { config, getBackendSrv } from '@grafana/runtime';
import { AppEvents } from '@grafana/data';
import { getAppEvents } from '@grafana/runtime';
import { Button, Stack, Box, TextArea, Field, Input, Alert } from '@grafana/ui';
import { AnnoKeyRepoName, AnnoKeyRepoPath } from 'app/features/apiserver/types';
import { DashboardMeta } from 'app/types';
import { useUpdateRepositoryFilesMutation } from '../../provisioning/api';
import { DashboardScene } from '../scene/DashboardScene';
import { SaveDashboardDrawer } from './SaveDashboardDrawer';
import { SaveDashboardFormCommonOptions } from './SaveDashboardForm';
import { DashboardChangeInfo } from './shared';
type FormData = {
ref: string;
path: string;
comment?: string;
repo: string;
};
function getDefaultValues(meta: DashboardMeta) {
const anno = meta.k8s?.annotations;
let ref = '';
let path = anno?.[AnnoKeyRepoPath] ?? '';
const repo = anno?.[AnnoKeyRepoName] ?? '';
const idx = path.indexOf('#');
if (idx > 0) {
ref = path.substring(idx + 1);
path = path.substring(0, idx);
}
return { ref, path, repo, comment: '' };
}
export interface Props {
meta: DashboardMeta;
dashboard: DashboardScene;
@@ -19,55 +41,38 @@ export interface Props {
changeInfo: DashboardChangeInfo;
}
export function SaveProvisionedDashboard({ meta, drawer, changeInfo }: Props) {
const dashboardJSON = useMemo(() => JSON.stringify(changeInfo.changedSaveModel, null, 2), [changeInfo]);
const [repo, setRepo] = useState<string>();
const [path, setPath] = useState<string>();
const [ref, setRef] = useState<string>();
const [comment, setComment] = useState<string>();
export function SaveProvisionedDashboard({ meta, drawer, changeInfo, dashboard }: Props) {
const [saveDashboard, request] = useUpdateRepositoryFilesMutation();
const { register, handleSubmit } = useForm({ defaultValues: getDefaultValues(meta) });
useEffect(() => {
const anno = meta.k8s?.annotations;
if (!anno) {
setRepo('');
setPath('');
setRef('');
const appEvents = getAppEvents();
if (request.isSuccess) {
appEvents.publish({
type: AppEvents.alertSuccess.name,
payload: ['Dashboard saved'],
});
dashboard.setState({ isDirty: false });
// TODO Avoid full reload
window.location.reload();
} else if (request.isError) {
appEvents.publish({
type: AppEvents.alertError.name,
payload: ['Error saving dashboard', request.error],
});
}
}, [request.isSuccess, request.isError, request.error, dashboard]);
const doSave = ({ ref, path, comment, repo }: FormData) => {
if (!repo || !path) {
return;
}
let ref = '';
let path = anno[AnnoKeyRepoPath] ?? '';
const idx = path.indexOf('#');
if (idx > 0) {
ref = path.substring(idx + 1);
path = path.substring(0, idx);
}
setRepo(anno[AnnoKeyRepoName]);
setPath(path);
setRef(ref);
}, [meta]);
const doSave = () => {
const url = `apis/provisioning.grafana.app/v0alpha1/namespaces/${config.namespace}/repositories/${repo}/files/${path}`;
const params: Record<string, string> = {};
if (ref) {
params['ref'] = ref;
}
if (comment) {
params['comment'] = comment;
}
getBackendSrv()
.put(url, dashboardJSON, { params })
.then((v) => {
console.log('WROTE', v);
alert('WROTE value');
});
saveDashboard({ ref, name: repo, path, message: comment, body: changeInfo.changedSaveModel });
};
return (
<div className={styles.container}>
<Stack direction="column" gap={2} grow={1}>
<form onSubmit={handleSubmit(doSave)}>
<Stack direction="column" gap={2}>
<div>
<Alert severity="warning" title="Development feature">
More warnings here... mostly exploratory interfaces.
@@ -77,34 +82,21 @@ export function SaveProvisionedDashboard({ meta, drawer, changeInfo }: Props) {
<SaveDashboardFormCommonOptions drawer={drawer} changeInfo={changeInfo} />
<Field label="Repository">
<div>{repo}</div>
<Input {...register('repo')} readOnly />
</Field>
<Field label="Path" description="File path inside the repository. This must be .json or .yaml">
<Input
value={path}
onChange={(e) => {
setPath(e.currentTarget.value);
}}
/>
<Input {...register('path')} />
</Field>
<Field label="Branch" description="only supported by github right now">
<Input
value={ref}
onChange={(e) => {
setRef(e.currentTarget.value);
}}
/>
<Field label="Branch" description="Only supported by GitHub right now">
<Input {...register('ref')} />
</Field>
<Field label="Comment">
<TextArea
{...register('comment')}
aria-label="comment"
value={comment ?? ''}
onChange={(e) => {
setComment(e.currentTarget.value);
}}
placeholder="Add a note to describe your changes (optional)."
autoFocus
rows={5}
@@ -113,7 +105,7 @@ export function SaveProvisionedDashboard({ meta, drawer, changeInfo }: Props) {
<Box paddingTop={2}>
<Stack gap={2}>
<Button variant="primary" onClick={doSave}>
<Button variant="primary" type="submit">
Save
</Button>
<Button variant="secondary" onClick={drawer.onClose} fill="outline">
@@ -122,13 +114,6 @@ export function SaveProvisionedDashboard({ meta, drawer, changeInfo }: Props) {
</Stack>
</Box>
</Stack>
</div>
</form>
);
}
const styles = {
container: css({
height: '100%',
display: 'flex',
}),
};
@@ -6,7 +6,6 @@ import {
UpdateRequestArg,
HelloWorld,
RepositoryForCreate,
WatchEvent,
WebhookResponse,
ResourceWrapper,
FileOperationArg,
@@ -55,47 +54,50 @@ const injectedRtkApi = api.injectEndpoints({
}),
invalidatesTags: ['RepositoryList'],
}),
connectGetRepositoryExport: build.query<ResourceWrapper, RequestArg>({
getRepositoryExport: build.query<ResourceWrapper, RequestArg>({
query: ({ name }) => ({
url: `${BASE_PATH}/${name}/export`,
}),
}),
connectPostRepositoryExport: build.mutation<ResourceWrapper, RequestArg>({
createRepositoryExport: build.mutation<ResourceWrapper, RequestArg>({
query: ({ name }) => ({
url: `${BASE_PATH}/${name}/export`,
method: 'POST',
}),
}),
connectGetRepositoryFiles: build.query<ResourceWrapper, GetFileArg>({
query: ({ name, path, commit }) => ({
getRepositoryFiles: build.query<ResourceWrapper, GetFileArg>({
query: ({ name, path, ref }) => ({
url: `${BASE_PATH}/${name}/files/${path}`,
params: { commit },
params: { ref },
}),
}),
connectPutRepositoryFiles: build.mutation<ResourceWrapper, FileOperationArg>({
query: ({ name, path, body, message }) => ({
updateRepositoryFiles: build.mutation<ResourceWrapper, FileOperationArg>({
query: ({ name, path, body, ref, message }) => ({
url: `${BASE_PATH}/${name}/files/${path}`,
method: 'PUT',
body,
params: { message },
params: { ref, message },
}),
}),
connectPostRepositoryFiles: build.mutation<ResourceWrapper, FileOperationArg>({
query: ({ name, path, body, message }) => ({
createRepositoryFiles: build.mutation<ResourceWrapper, FileOperationArg>({
query: ({ name, path, body, ref, message }) => ({
url: `${BASE_PATH}/${name}/files/${path}`,
method: 'POST',
body,
params: { message },
params: { ref, message },
}),
}),
connectDeleteRepositoryFiles: build.mutation<ResourceWrapper, { name: string; path: string; message?: string }>({
query: ({ name, path, message }) => ({
deleteRepositoryFiles: build.mutation<
ResourceWrapper,
{ name: string; path: string; ref?: string; message?: string }
>({
query: ({ name, path, ref, message }) => ({
url: `${BASE_PATH}/${name}/files/${path}`,
method: 'DELETE',
params: { message },
params: { ref, message },
}),
}),
connectGetRepositoryHello: build.query<HelloWorld, { name: string; whom?: string }>({
getRepositoryHello: build.query<HelloWorld, { name: string; whom?: string }>({
query: ({ name, whom }) => ({
url: `${BASE_PATH}/${name}/hello`,
params: { whom },
@@ -121,29 +123,17 @@ const injectedRtkApi = api.injectEndpoints({
body,
}),
}),
connectGetRepositoryWebhook: build.query<WebhookResponse, RequestArg>({
getRepositoryWebhook: build.query<WebhookResponse, RequestArg>({
query: ({ name }) => ({
url: `${BASE_PATH}/${name}/webhook`,
}),
}),
connectPostRepositoryWebhook: build.mutation<WebhookResponse, RequestArg>({
createRepositoryWebhook: build.mutation<WebhookResponse, RequestArg>({
query: ({ name }) => ({
url: `${BASE_PATH}/${name}/webhook`,
method: 'POST',
}),
}),
watchRepositoryList: build.query<WatchEvent, void>({
query: () => ({
url: `${BASE_PATH}`,
params: { watch: true },
}),
}),
watchRepository: build.query<WatchEvent, RequestArg>({
query: ({ name }) => ({
url: `${BASE_PATH}/${name}`,
params: { watch: true },
}),
}),
}),
overrideExisting: false,
});
@@ -157,18 +147,16 @@ export const {
useUpdateRepositoryMutation,
useDeleteRepositoryMutation,
usePatchRepositoryMutation,
useConnectGetRepositoryExportQuery,
useConnectPostRepositoryExportMutation,
useConnectGetRepositoryFilesQuery,
useConnectPutRepositoryFilesMutation,
useConnectPostRepositoryFilesMutation,
useConnectDeleteRepositoryFilesMutation,
useConnectGetRepositoryHelloQuery,
useGetRepositoryExportQuery,
useCreateRepositoryExportMutation,
useGetRepositoryFilesQuery,
useUpdateRepositoryFilesMutation,
useCreateRepositoryFilesMutation,
useDeleteRepositoryFilesMutation,
useGetRepositoryHelloQuery,
useGetRepositoryStatusQuery,
useUpdateRepositoryStatusMutation,
usePatchRepositoryStatusMutation,
useConnectGetRepositoryWebhookQuery,
useConnectPostRepositoryWebhookMutation,
useWatchRepositoryListQuery,
useWatchRepositoryQuery,
useGetRepositoryWebhookQuery,
useCreateRepositoryWebhookMutation,
} = injectedRtkApi;
@@ -89,10 +89,11 @@ export type FileOperationArg = {
path: string;
body: object;
message?: string;
ref?: string;
};
export type GetFileArg = {
name: string;
path: string;
commit?: string;
ref?: string;
};
@@ -1,10 +1,12 @@
import { getBackendSrv } from '@grafana/runtime';
import { DashboardDTO } from 'app/types';
import { AnnoKeyRepoName, AnnoKeyRepoPath } from '../apiserver/types';
import { BASE_URL } from './api/baseAPI';
import { AnnoKeyRepoName, AnnoKeyRepoPath, Resource } from '../apiserver/types';
/**
*
* Load a dashboard from repository
*/
export async function loadDashboardFromProvisioning(repo: string, path: string): Promise<DashboardDTO> {
@@ -26,14 +28,14 @@ export async function loadDashboardFromProvisioning(repo: string, path: string):
}
// Make sure the annotation key exists
let anno = dryRun.metadata.annotations
let anno = dryRun.metadata.annotations;
if (!anno) {
dryRun.metadata.annotations = anno = {}
dryRun.metadata.annotations = anno = {};
}
anno[AnnoKeyRepoName] = repo
anno[AnnoKeyRepoPath] = path
anno[AnnoKeyRepoName] = repo;
anno[AnnoKeyRepoPath] = path;
if (ref) {
anno[AnnoKeyRepoPath] += path + '#' + ref
anno[AnnoKeyRepoPath] += path + '#' + ref;
}
return {