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 <ryantxu@gmail.com>
This commit is contained in:
Alex KhomenkoandRyan McKinley authored and GitHub committed 2025-02-04 10:32:53 +02:00
1 parent ca3db84d99
commit 66d4ee97f0
16 files changed
+96 -55

No files matched your search

@@ -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<HTMLDivElement> {
text: React.ReactNode;
@@ -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 (
<Text color={'secondary'}>
{' '}
| <Icon name={icon} /> {repoName}
</Text>
);
return <Badge text={'Provisioned'} color={'darkgrey'} />;
// const [repoName, icon] = getRepoDetails(repo);
//
// return (
// <Text color={'secondary'}>
// {' '}
// | <Icon name={icon} /> {repoName}
// </Text>
// );
}
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'];
// }
// }
@@ -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,
@@ -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;
@@ -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 (
<Stack alignItems={'center'} gap={2}>
<Text element={'h1'}>{title}</Text> <FolderRepo repo={folder?.repository} />
</Stack>
);
};
return (
<Page
navId="dashboards/browse"
pageNav={navModel}
onEditTitle={showEditTitle ? onEditTitle : undefined}
renderTitle={renderTitle}
actions={
<>
{config.featureToggles.dashboardRestore && hasAdminRights && (
@@ -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<FolderListItemDTO[]>;
async function addRepositoryData(data: FolderDTO): Promise<FolderDTO>;
async function addRepositoryData(data: FolderListItemDTO[] | FolderDTO): Promise<FolderListItemDTO[] | FolderDTO> {
export async function addRepositoryData(data: FolderListItemDTO[]): Promise<FolderListItemDTO[]>;
export async function addRepositoryData(data: NestedFolderDTO[]): Promise<NestedFolderDTO[]>;
export async function addRepositoryData(data: NestedFolderDTO): Promise<NestedFolderDTO>;
export async function addRepositoryData(data: FolderDTO): Promise<FolderDTO>;
export async function addRepositoryData(
data: FolderDataType | FolderDataType[]
): Promise<FolderDataType | FolderDataType[]> {
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 = <T extends FolderListItemDTO | FolderDTO>(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);
}
@@ -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),
@@ -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)}
/>
);
}
@@ -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
)}
</Text>
<FolderRepo repo={item.repository} />
</div>
</>
);
@@ -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<DashboardViewItem, 'kind' | 'uid' | 'parentUID'>; isSelected: boolean }>
action: PayloadAction<{
item: Pick<DashboardViewItem, 'kind' | 'uid' | 'parentUID' | 'repository'>;
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;
}
@@ -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) {
<Field label="Folder">
<FolderPicker
onChange={(uid: string | undefined, title: string | undefined, repository?: Repository) => {
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);
@@ -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 (
<FolderPicker
onChange={(uid?: string, title?: string, repository?: Repository) => {
onChange={(uid?: string, title?: string, repository?: RepositoryView) => {
onChange({ uid, title });
const name = repository?.metadata?.name;
const name = repository?.name;
if (name) {
setValue('repo', name);
}
@@ -136,7 +136,9 @@ export function ToolbarActions({ dashboard }: Props) {
group: 'icon-actions',
condition: true,
render: () => {
return <Badge color="green" icon="exchange-alt" text="Provisioned" key="provisioned-dashboard-button-badge" />;
return (
<Badge color="darkgrey" icon="exchange-alt" text="Provisioned" key="provisioned-dashboard-button-badge" />
);
},
});
}
@@ -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;
}
+2 -2
View File
@@ -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 {
+3 -4
View File
@@ -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<FolderDTO, 'title' | 'uid' | 'url'>;
@@ -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[];