POC/Provisioning: Fix saving in nested folders (#100193)

* Remove saveProvisioned flag

* Get repo data from resource

* Refactor

* get repository data for nested folder

* Remove unused hook

* Sort configs alphabetically

* Codeformat
This commit is contained in:
Alex Khomenko authored and GitHub committed 2025-02-06 18:02:13 +02:00
1 parent f5725e1b1e
commit a7deb70096
6 files changed
+46 -47

No files matched your search

@@ -1,10 +1,9 @@
import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneObjectRef } from '@grafana/scenes';
import { Drawer, Tab, TabsBar } from '@grafana/ui';
import { useUrlParams } from 'app/core/navigation/hooks';
import { AnnoKeyRepoName } from 'app/features/apiserver/types';
import { SaveDashboardDiff } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDiff';
import { useFolderRepository } from '../../provisioning/hooks';
import { useGetResourceRepository } from '../../provisioning/hooks';
import { DashboardScene } from '../scene/DashboardScene';
import { SaveDashboardAsForm } from './SaveDashboardAsForm';
@@ -20,7 +19,6 @@ interface SaveDashboardDrawerState extends SceneObjectState {
saveRefresh?: boolean;
saveAsCopy?: boolean;
onSaveSuccess?: () => void;
saveProvisioned?: boolean;
}
export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerState> {
@@ -47,22 +45,21 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
};
static Component = ({ model }: SceneComponentProps<SaveDashboardDrawer>) => {
const { saveProvisioned, showDiff, saveAsCopy, saveTimeRange, saveVariables, saveRefresh } = model.useState();
const { showDiff, saveAsCopy, saveTimeRange, saveVariables, saveRefresh } = model.useState();
const changeInfo = model.state.dashboardRef
.resolve()
.getDashboardChanges(saveTimeRange, saveVariables, saveRefresh);
const { changedSaveModel, initialSaveModel, diffs, diffCount, hasFolderChanges } = changeInfo;
const changesCount = diffCount + (hasFolderChanges ? 1 : 0);
const dashboard = model.state.dashboardRef.resolve();
const { meta } = dashboard.useState();
const { provisioned: isProvisioned, folderTitle } = meta;
const [params] = useUrlParams();
const folderUid = params.get('folderUid');
const folderRepository = useFolderRepository(folderUid ?? undefined);
// Provisioned dashboards have k8s metadata annotations
const isProvisionedNG = saveProvisioned || meta.k8s?.annotations?.[AnnoKeyRepoName] || Boolean(folderRepository);
const folderUid = params.get('folderUid') || undefined;
const folderRepository = useGetResourceRepository({ folderUid });
const isProvisionedNG = dashboard.isProvisioned() || Boolean(folderRepository);
const tabs = (
<TabsBar>
@@ -41,7 +41,6 @@ export interface Props {
}
export function SaveProvisionedDashboard({ drawer, changeInfo, dashboard }: Props) {
const { saveProvisioned } = drawer.useState();
const { meta, title: defaultTitle, description: defaultDescription } = dashboard.useState();
const prURL = usePullRequestParam();
@@ -51,7 +50,7 @@ export function SaveProvisionedDashboard({ drawer, changeInfo, dashboard }: Prop
isGitHub,
repositoryConfig,
} = useDefaultValues({ meta, defaultTitle, defaultDescription });
const [action, request] = useCreateOrUpdateRepositoryFile(saveProvisioned || isNew ? undefined : defaultValues.path);
const [action, request] = useCreateOrUpdateRepositoryFile(isNew ? undefined : defaultValues.path);
const {
register,
handleSubmit,
@@ -207,7 +206,7 @@ export function SaveProvisionedDashboard({ drawer, changeInfo, dashboard }: Prop
</Alert>
)}
<Stack gap={2}>
<Button variant="primary" type="submit" disabled={(request.isLoading || !isDirty) && !saveProvisioned}>
<Button variant="primary" type="submit" disabled={request.isLoading || !isDirty}>
{request.isLoading ? 'Saving...' : 'Save'}
</Button>
<Button variant="secondary" onClick={drawer.onClose} fill="outline">
@@ -218,6 +217,7 @@ export function SaveProvisionedDashboard({ drawer, changeInfo, dashboard }: Prop
</form>
);
}
const BranchValidationError = () => {
return (
<>
@@ -231,6 +231,7 @@ const BranchValidationError = () => {
</>
);
};
async function validateDashboardName(title: string, formValues: FormData) {
if (title === formValues.folder.title?.trim()) {
return 'Dashboard name cannot be the same as folder name';
@@ -1,5 +1,5 @@
import { AnnoKeyRepoName, AnnoKeyRepoPath } from 'app/features/apiserver/types';
import { useFolderRepository } from 'app/features/provisioning/hooks';
import { useGetResourceRepository } from 'app/features/provisioning/hooks';
import { DashboardMeta } from 'app/types';
import { getDefaultWorkflow } from './defaults';
@@ -21,20 +21,19 @@ function generatePath(timestamp: number, pathFromAnnotation?: string, slug?: str
}
export function useDefaultValues({ meta, defaultTitle, defaultDescription }: UseDefaultValuesParams) {
// ???? why is this getting the repository from the folder, not the file???
// Should we only get it from the folder when new?
const folderRepository = useFolderRepository(meta.folderUid);
const timestamp = Date.now();
const annotations = meta.k8s?.annotations;
const annoName = annotations?.[AnnoKeyRepoName];
const annoPath = annotations?.[AnnoKeyRepoPath];
const repositoryConfig = folderRepository?.spec;
const repositoryName = folderRepository?.metadata?.name ?? '';
// Get config by resource name or folder UID for new resources
const repositoryConfig = useGetResourceRepository({ name: annoName, folderUid: meta.folderUid });
const repository = repositoryConfig?.spec;
const timestamp = Date.now();
return {
values: {
ref: `dashboard/${timestamp}`,
path: generatePath(timestamp, annoPath, meta.slug),
repo: annotations?.[AnnoKeyRepoName] ?? repositoryName,
repo: annoName || repositoryConfig?.metadata?.name || '',
comment: '',
folder: {
uid: meta.folderUid,
@@ -42,10 +41,10 @@ export function useDefaultValues({ meta, defaultTitle, defaultDescription }: Use
},
title: defaultTitle,
description: defaultDescription ?? '',
workflow: getDefaultWorkflow(repositoryConfig),
workflow: getDefaultWorkflow(repository),
},
isNew: !annoPath,
repositoryConfig,
isGitHub: repositoryConfig?.type === 'github',
isNew: !annoName,
repositoryConfig: repository,
isGitHub: repository?.type === 'github',
};
}
@@ -395,15 +395,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
return true;
};
public openSaveDrawer({
saveAsCopy,
saveProvisioned,
onSaveSuccess,
}: {
saveAsCopy?: boolean;
saveProvisioned?: boolean;
onSaveSuccess?: () => void;
}) {
public openSaveDrawer({ saveAsCopy, onSaveSuccess }: { saveAsCopy?: boolean; onSaveSuccess?: () => void }) {
if (!this.state.isEditing) {
return;
}
@@ -413,7 +405,6 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
dashboardRef: this.getRef(),
saveAsCopy,
onSaveSuccess,
saveProvisioned,
}),
});
}
@@ -608,7 +608,7 @@ export function ToolbarActions({ dashboard }: Props) {
return (
<Button
onClick={() => {
dashboard.openSaveDrawer({ saveProvisioned: isProvisionedNG });
dashboard.openSaveDrawer({});
}}
className={styles.buttonWithExtraMargin}
tooltip={t('dashboard.toolbar.save-dashboard.tooltip', 'Save changes')}
+23 -12
View File
@@ -1,17 +1,20 @@
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,
RepositorySpec,
ReplaceRepositoryFilesWithPathArg,
useReplaceRepositoryMutation,
ListRepositoryArg,
} from './api';
export function useCreateOrUpdateRepository(name?: string) {
@@ -51,12 +54,15 @@ export function useCreateOrUpdateRepository(name?: string) {
return [updateOrCreate, name ? updateRequest : createRequest] as const;
}
// Sort repositories by resourceVersion to show the last modified
// 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) => {
return Number(b.metadata?.resourceVersion) - Number(a.metadata?.resourceVersion);
const titleA = a.spec?.title ?? '';
const titleB = b.spec?.title ?? '';
return collator.compare(titleA, titleB);
});
return [sortedItems, query.isLoading];
@@ -88,16 +94,21 @@ export const usePullRequestParam = () => {
return decodeURIComponent(prParam);
};
export const useFolderRepository = (folderUid?: string) => {
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);
if (!folderUid) {
const repoName = name || folderQuery.data?.repository?.name;
if (!items?.length || isLoading || !repoName) {
return undefined;
}
if (!items?.length || isLoading || !folderUid) {
return undefined;
}
return items.find((repo) => repo.metadata?.name === folderUid);
return items.find((repo) => repo.metadata?.name === repoName);
};