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:
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,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 {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in new issue
Block a user