From 66d4ee97f0223f85195894f6d013d80fc6b0a1ec Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Tue, 4 Feb 2025 10:32:53 +0200 Subject: [PATCH] POC/Provisioning: Marked folder as provisioned (#100028) * Add repo data to nested folders * Fix namespace filter * Update spec * Use settings endpoint * Update comment * Update filter * Use RepositoryView * frontend settings as list * frontend settings as list * remove weird comment * Use badge * Badge: Add darkgrey color * Use updated settings data * Do not allow selecting provisioned folders * Update Badge color * Add provisioned info to title * Cleanup * Cleanup --------- Co-authored-by: Ryan McKinley --- .../grafana-ui/src/components/Badge/Badge.tsx | 2 +- .../NestedFolderPicker/FolderRepo.tsx | 48 ++++++++++--------- .../NestedFolderPicker/NestedFolderList.tsx | 3 ++ .../NestedFolderPicker/NestedFolderPicker.tsx | 4 +- .../BrowseDashboardsPage.tsx | 17 +++++-- .../api/browseDashboardsAPI.ts | 30 ++++++++---- .../browse-dashboards/api/services.ts | 5 ++ .../components/CheckboxCell.tsx | 1 + .../browse-dashboards/components/NameCell.tsx | 3 ++ .../browse-dashboards/state/reducers.ts | 9 ++-- .../saving/SaveDashboardAsForm.tsx | 6 +-- .../provisioned/SaveProvisionedDashboard.tsx | 6 +-- .../scene/NavToolbarActions.tsx | 4 +- public/app/features/search/service/types.ts | 2 + public/app/features/search/types.ts | 4 +- public/app/types/folders.ts | 7 ++- 16 files changed, 96 insertions(+), 55 deletions(-) diff --git a/packages/grafana-ui/src/components/Badge/Badge.tsx b/packages/grafana-ui/src/components/Badge/Badge.tsx index 9238c8a648c..73eca08b108 100644 --- a/packages/grafana-ui/src/components/Badge/Badge.tsx +++ b/packages/grafana-ui/src/components/Badge/Badge.tsx @@ -12,7 +12,7 @@ import { SkeletonComponent, attachSkeleton } from '../../utils/skeleton'; import { Icon } from '../Icon/Icon'; import { Tooltip } from '../Tooltip/Tooltip'; -export type BadgeColor = 'blue' | 'red' | 'green' | 'orange' | 'purple'; +export type BadgeColor = 'blue' | 'red' | 'green' | 'orange' | 'purple' | 'darkgrey'; export interface BadgeProps extends HTMLAttributes { text: React.ReactNode; diff --git a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx index 990ef952f85..e9f095b5d49 100644 --- a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx +++ b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx @@ -1,33 +1,35 @@ -import { Icon, IconName, Text } from '@grafana/ui'; -import { Repository, RepositorySpec } from 'app/features/provisioning/api'; +import { Badge } from '@grafana/ui'; +import { RepositoryView } from 'app/features/provisioning/api'; export interface Props { - repo?: Repository; + repo?: RepositoryView | string; } export function FolderRepo({ repo }: Props) { - if (!repo?.spec) { + if (!repo) { return null; } - const [repoName, icon] = getRepoDetails(repo.spec); - return ( - - {' '} - | {repoName} - - ); + return ; + // const [repoName, icon] = getRepoDetails(repo); + // + // return ( + // + // {' '} + // | {repoName} + // + // ); } -function getRepoDetails(spec: RepositorySpec): [string, IconName] { - switch (spec.type) { - case 'github': - return [spec.github?.repository ?? '', 'github']; - case 'local': - return [spec.local?.path ?? '', 'file-blank']; - case 's3': - return [spec.s3?.bucket ?? '', 'cloud']; - default: - return ['', 'question-circle']; - } -} +// function getRepoDetails(spec: RepositoryView): [string, IconName] { +// switch (spec.type) { +// case 'github': +// return [spec.title ?? '', 'github']; +// case 'local': +// return [spec.title ?? '', 'file-blank']; +// case 's3': +// return [spec.title ?? '', 'cloud']; +// default: +// return ['', 'question-circle']; +// } +// } diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx index fdff5047753..04ef5b5b715 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderList.tsx @@ -279,6 +279,9 @@ const getStyles = (theme: GrafanaTheme2) => { rowBody, label: css({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), lineHeight: ROW_HEIGHT + 'px', flexGrow: 1, minWidth: 0, diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx index ba4528ed105..2db0129eeff 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx @@ -10,7 +10,7 @@ import { Alert, Icon, Input, LoadingBar, Stack, Text, useStyles2 } from '@grafan import { t } from 'app/core/internationalization'; import { skipToken, useGetFolderQuery } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { DashboardViewItemWithUIItems, DashboardsTreeItem } from 'app/features/browse-dashboards/types'; -import { Repository } from 'app/features/provisioning/api'; +import { RepositoryView } from 'app/features/provisioning/api'; import { getGrafanaSearcher } from 'app/features/search/service/searcher'; import { QueryResponse } from 'app/features/search/service/types'; import { queryResultToViewItem } from 'app/features/search/service/utils'; @@ -40,7 +40,7 @@ export interface NestedFolderPickerProps { permission?: PermissionLevelString.View | PermissionLevelString.Edit; /* Callback for when the user selects a folder */ - onChange?: (folderUID: string | undefined, folderName: string | undefined, repository?: Repository) => void; + onChange?: (folderUID: string | undefined, folderName: string | undefined, repository?: RepositoryView) => void; /* Whether the picker should be clearable */ clearable?: boolean; diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index 12b31ff4974..cb222a73867 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -5,12 +5,13 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; -import { LinkButton, FilterInput, useStyles2 } from '@grafana/ui'; +import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { getConfig } from 'app/core/config'; import { Trans } from 'app/core/internationalization'; import { useDispatch } from 'app/types'; +import { FolderRepo } from '../../core/components/NestedFolderPicker/FolderRepo'; import { contextSrv } from '../../core/services/context_srv'; import { buildNavModel, getDashboardsTabID } from '../folders/state/navModel'; import { useSearchStateManager } from '../search/state/SearchStateManager'; @@ -91,8 +92,8 @@ const BrowseDashboardsPage = memo(() => { const { canEditFolders, canEditDashboards, canCreateDashboards, canCreateFolders } = getFolderPermissions(folder); const hasAdminRights = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin; - const showEditTitle = canEditFolders && folderUID; - const canSelect = canEditFolders || canEditDashboards; + const showEditTitle = canEditFolders && folderUID && !folder?.repository; + const canSelect = (canEditFolders || canEditDashboards) && !folder?.repository; const onEditTitle = async (newValue: string) => { if (folderDTO) { const result = await saveFolder({ @@ -117,11 +118,21 @@ const BrowseDashboardsPage = memo(() => { origin: window.location.pathname === getConfig().appSubUrl + '/dashboards' ? 'Dashboards' : 'Folder view', }); }; + + const renderTitle = (title: string) => { + return ( + + {title} + + ); + }; + return ( {config.featureToggles.dashboardRestore && hasAdminRights && ( diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 369bd1bb25e..3e3aa805a52 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -11,6 +11,7 @@ import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { isV1DashboardCommand, isV2DashboardCommand } from 'app/features/dashboard/api/utils'; import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; +import { provisioningAPI, RepositoryViewList } from 'app/features/provisioning/api'; import { dispatch } from 'app/store/store'; import { DashboardDTO, @@ -24,7 +25,7 @@ import { } from 'app/types'; import { t } from '../../../core/internationalization'; -import { provisioningAPI, RepositoryList } from '../../provisioning/api'; +import { NestedFolderDTO } from '../../search/service/types'; import { refetchChildren, refreshParents } from '../state'; import { DashboardTreeSelection } from '../types'; @@ -441,27 +442,36 @@ export const { export { skipToken } from '@reduxjs/toolkit/query/react'; +type FolderDataType = FolderListItemDTO | NestedFolderDTO | FolderDTO; + // Overloaded function signatures to handle different input types -async function addRepositoryData(data: FolderListItemDTO[]): Promise; -async function addRepositoryData(data: FolderDTO): Promise; -async function addRepositoryData(data: FolderListItemDTO[] | FolderDTO): Promise { +export async function addRepositoryData(data: FolderListItemDTO[]): Promise; +export async function addRepositoryData(data: NestedFolderDTO[]): Promise; +export async function addRepositoryData(data: NestedFolderDTO): Promise; +export async function addRepositoryData(data: FolderDTO): Promise; +export async function addRepositoryData( + data: FolderDataType | FolderDataType[] +): Promise { if (!config.featureToggles.provisioning) { return data; } - const repositories: RepositoryList = await dispatch(provisioningAPI.endpoints.listRepository.initiate({})).unwrap(); + const settings: RepositoryViewList = await dispatch( + provisioningAPI.endpoints.getFrontendSettings.initiate() + ).unwrap(); - if (!repositories.items?.length) { + if (!settings.items.length) { return data; } - const enrichItem = (item: T) => { - const repository = repositories.items?.find((repo) => repo.metadata?.name === item.uid); // folder UID matches + const addRepositoryToItem = (item: FolderDataType) => { + const repository = settings.items.find((repo) => repo.name === item.uid); return repository ? { ...item, repository } : item; }; if (Array.isArray(data)) { - return data.map(enrichItem); + return data.map(addRepositoryToItem); } - return enrichItem(data); + + return addRepositoryToItem(data); } diff --git a/public/app/features/browse-dashboards/api/services.ts b/public/app/features/browse-dashboards/api/services.ts index 1c4c8a47e15..9e311c338ee 100644 --- a/public/app/features/browse-dashboards/api/services.ts +++ b/public/app/features/browse-dashboards/api/services.ts @@ -9,6 +9,8 @@ import { contextSrv } from '../../../core/core'; import { AccessControlAction } from '../../../types'; import { getFolderURL, isSharedWithMe } from '../components/utils'; +import { addRepositoryData } from './browseDashboardsAPI'; + export const PAGE_SIZE = 50; export async function listFolders( @@ -31,6 +33,8 @@ export async function listFolders( page, limit: pageSize, }); + + folders = await addRepositoryData(folders); } return folders.map((item) => ({ @@ -39,6 +43,7 @@ export async function listFolders( title: item.title, parentTitle, parentUID, + repository: item.repository, // URLs from the backend come with subUrlPrefix already included, so match that behaviour here url: isSharedWithMe(item.uid) ? undefined : getFolderURL(item.uid), diff --git a/public/app/features/browse-dashboards/components/CheckboxCell.tsx b/public/app/features/browse-dashboards/components/CheckboxCell.tsx index 26325c23f49..e74a29ff4b3 100644 --- a/public/app/features/browse-dashboards/components/CheckboxCell.tsx +++ b/public/app/features/browse-dashboards/components/CheckboxCell.tsx @@ -41,6 +41,7 @@ export default function CheckboxCell({ value={state === SelectionState.Selected} indeterminate={state === SelectionState.Mixed} onChange={(ev) => onItemSelectionChange?.(item, ev.currentTarget.checked)} + disabled={Boolean(item.repository)} /> ); } diff --git a/public/app/features/browse-dashboards/components/NameCell.tsx b/public/app/features/browse-dashboards/components/NameCell.tsx index dbdc60eba51..3ff89ed6f26 100644 --- a/public/app/features/browse-dashboards/components/NameCell.tsx +++ b/public/app/features/browse-dashboards/components/NameCell.tsx @@ -9,6 +9,7 @@ import { t } from 'app/core/internationalization'; import { getIconForItem } from 'app/features/search/service/utils'; import { Indent } from '../../../core/components/Indent/Indent'; +import { FolderRepo } from '../../../core/components/NestedFolderPicker/FolderRepo'; import { useChildrenByParentUIDState } from '../state'; import { DashboardsTreeCellProps } from '../types'; @@ -102,6 +103,8 @@ export function NameCell({ row: { original: data }, onFolderClick, treeID }: Nam item.title )} + + ); diff --git a/public/app/features/browse-dashboards/state/reducers.ts b/public/app/features/browse-dashboards/state/reducers.ts index 045be88a490..862ccf5edb4 100644 --- a/public/app/features/browse-dashboards/state/reducers.ts +++ b/public/app/features/browse-dashboards/state/reducers.ts @@ -83,12 +83,15 @@ export function setItemSelectionState( // SearchView doesn't use DashboardViewItemKind (yet), so we pick just the specific properties // we're interested in - action: PayloadAction<{ item: Pick; isSelected: boolean }> + action: PayloadAction<{ + item: Pick; + isSelected: boolean; + }> ) { const { item, isSelected } = action.payload; // UI shouldn't allow it, but also prevent sharedwithme from being selected - if (isSharedWithMe(item.uid)) { + if (isSharedWithMe(item.uid) || Boolean(item.repository)) { return; } @@ -170,7 +173,7 @@ export function setAllSelection( for (const child of collection.items) { // Don't traverse into the sharedwithme folder - if (isSharedWithMe(child.uid)) { + if (isSharedWithMe(child.uid) || Boolean(child.repository)) { continue; } diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx index 28e4c5d3357..486041d1d6b 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx @@ -8,7 +8,7 @@ import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; import { AnnoKeyRepoName } from '../../apiserver/types'; -import { Repository } from '../../provisioning/api'; +import { RepositoryView } from '../../provisioning/api'; import { DashboardScene } from '../scene/DashboardScene'; import { DashboardChangeInfo, NameAlreadyExistsError, SaveButton, isNameExistsError } from './shared'; @@ -132,8 +132,8 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { { - const name = repository?.metadata?.name; + onChange={(uid: string | undefined, title: string | undefined, repository?: RepositoryView) => { + const name = repository?.name; setValue('folder', { uid, title }); const folderUid = dashboard.state.meta.folderUid; setHasFolderChanged(uid !== folderUid); diff --git a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboard.tsx b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboard.tsx index 49a9ab68165..379688aec72 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboard.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboard.tsx @@ -7,7 +7,7 @@ import { Alert, Button, Field, Input, RadioButtonGroup, Stack, TextArea, TextLin import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { AnnoKeyRepoName } from 'app/features/apiserver/types'; import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv'; -import { Repository } from 'app/features/provisioning/api'; +import { RepositoryView } from 'app/features/provisioning/api'; import { useCreateOrUpdateRepositoryFile, usePullRequestParam } from 'app/features/provisioning/hooks'; import { WorkflowOption } from 'app/features/provisioning/types'; import { createPRLink, validateBranchName } from 'app/features/provisioning/utils/git'; @@ -135,9 +135,9 @@ export function SaveProvisionedDashboard({ drawer, changeInfo, dashboard }: Prop render={({ field: { ref, value, onChange, ...field } }) => { return ( { + onChange={(uid?: string, title?: string, repository?: RepositoryView) => { onChange({ uid, title }); - const name = repository?.metadata?.name; + const name = repository?.name; if (name) { setValue('repo', name); } diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index e59cef6fe30..9a5d739c1f1 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -136,7 +136,9 @@ export function ToolbarActions({ dashboard }: Props) { group: 'icon-actions', condition: true, render: () => { - return ; + return ( + + ); }, }); } diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts index 387bbfc66ef..5235ff96cf8 100644 --- a/public/app/features/search/service/types.ts +++ b/public/app/features/search/service/types.ts @@ -1,5 +1,6 @@ import { DataFrameView, SelectableValue } from '@grafana/data'; import { TermCount } from 'app/core/components/TagFilter/TagFilter'; +import { RepositoryView } from 'app/features/provisioning/api'; import { PermissionLevelString } from 'app/types'; export interface FacetField { @@ -91,4 +92,5 @@ export interface GrafanaSearcher { export interface NestedFolderDTO { uid: string; title: string; + repository?: RepositoryView; } diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts index 07e921e230e..0332cf50130 100644 --- a/public/app/features/search/types.ts +++ b/public/app/features/search/types.ts @@ -2,7 +2,7 @@ import { Action } from 'redux'; import { WithAccessControlMetadata } from '@grafana/data'; -import { Repository } from '../provisioning/api'; +import { RepositoryView } from '../provisioning/api'; import { QueryResponse } from './service/types'; @@ -82,7 +82,7 @@ export interface DashboardViewItem { // For enterprise sort options sortMeta?: number | string; // value sorted by sortMetaName?: string; // name of the value being sorted e.g. 'Views' - repository?: Repository; + repository?: RepositoryView; } export interface SearchAction extends Action { diff --git a/public/app/types/folders.ts b/public/app/types/folders.ts index 28f333d65ed..8400193a0ee 100644 --- a/public/app/types/folders.ts +++ b/public/app/types/folders.ts @@ -1,11 +1,10 @@ import { WithAccessControlMetadata } from '@grafana/data'; - -import { Repository } from '../features/provisioning/api'; +import { RepositoryView } from 'app/features/provisioning/api'; export interface FolderListItemDTO { uid: string; title: string; - repository?: Repository; + repository?: RepositoryView; } export type FolderParent = Pick; @@ -20,7 +19,7 @@ export interface FolderDTO extends WithAccessControlMetadata { hasAcl: boolean; id: number; parentUid?: string; - repository?: Repository; + repository?: RepositoryView; // The API does actually return a full FolderDTO here, but we want to restrict it to just a few properties parents?: FolderParent[];