NestedFolders: Style tweaks (#68532)

* Center align cell items

* Change empty folder message + style

* Use new Text components in TypeCell

* Use new components in Move and Delete modals

* fix test

* Change spinner in DescendantsCount to the same font size as the text to prevent layout shift
This commit is contained in:
Josh Hunt 2023-05-22 09:59:46 +01:00 committed by GitHub
parent dc656ecd8a
commit e17f676a98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 63 additions and 77 deletions

View File

@ -4,17 +4,13 @@ import React from 'react';
import { stylesFactory } from '../../themes';
import { Icon } from '../Icon/Icon';
const getStyles = stylesFactory((size: number, inline: boolean) => {
return {
wrapper: css`
font-size: ${size}px;
${inline
? css`
display: inline-block;
`
: ''}
`,
};
const getStyles = stylesFactory((size: number | string, inline: boolean) => {
return css([
{
fontSize: typeof size === 'string' ? size : `${size}px`,
},
inline && { display: 'inline-block' },
]);
});
export type Props = {
@ -22,7 +18,7 @@ export type Props = {
style?: React.CSSProperties;
iconClassName?: string;
inline?: boolean;
size?: number;
size?: number | string;
};
/**
@ -31,7 +27,7 @@ export type Props = {
export const Spinner = ({ className, inline = false, iconClassName, style, size = 16 }: Props) => {
const styles = getStyles(size, inline);
return (
<div data-testid="Spinner" style={style} className={cx(styles.wrapper, className)}>
<div data-testid="Spinner" style={style} className={cx(styles, className)}>
<Icon className={cx('fa-spin', iconClassName)} name="fa fa-spinner" />
</div>
);

View File

@ -3,6 +3,7 @@ import { sortBy } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Space } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { Button, useStyles2 } from '@grafana/ui';
import { SlideDown } from 'app/core/components/Animations/SlideDown';
@ -143,6 +144,7 @@ export const Permissions = ({
$all: false,
}}
/>
<Space v={2} />
</>
)}
{canSetPermissions && (

View File

@ -1,8 +1,8 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { ConfirmModal, useStyles2 } from '@grafana/ui';
import { Space } from '@grafana/experimental';
import { ConfirmModal } from '@grafana/ui';
import { P } from '@grafana/ui/src/unstable';
import { DashboardTreeSelection } from '../../types';
@ -16,8 +16,6 @@ export interface Props {
}
export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => {
const styles = useStyles2(getStyles);
const onDelete = () => {
onConfirm();
onDismiss();
@ -26,10 +24,11 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P
return (
<ConfirmModal
body={
<div className={styles.modalBody}>
This action will delete the following content:
<>
<P>This action will delete the following content:</P>
<DescendantCount selectedItems={selectedItems} />
</div>
<Space v={2} />
</>
}
confirmationText="Delete"
confirmText="Delete"
@ -40,9 +39,3 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P
/>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
modalBody: css({
...theme.typography.body,
}),
});

View File

@ -1,8 +1,7 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, Spinner, useStyles2 } from '@grafana/ui';
import { Alert, Spinner, useTheme2 } from '@grafana/ui';
import { P } from '@grafana/ui/src/unstable';
import { useGetAffectedItemsQuery } from '../../api/browseDashboardsAPI';
import { DashboardTreeSelection } from '../../types';
@ -14,24 +13,17 @@ export interface Props {
}
export const DescendantCount = ({ selectedItems }: Props) => {
const styles = useStyles2(getStyles);
const theme = useTheme2();
const { data, isFetching, isLoading, error } = useGetAffectedItemsQuery(selectedItems);
return (
<div className={styles.breakdown}>
<>
{data && buildBreakdownString(data.folder, data.dashboard, data.libraryPanel, data.alertRule)}
{(isFetching || isLoading) && <Spinner size={12} />}
{error && <Alert severity="error" title="Unable to retrieve descendant information" />}
</>
</div>
<>
{data && (
<P color="secondary">{buildBreakdownString(data.folder, data.dashboard, data.libraryPanel, data.alertRule)}</P>
)}
{(isFetching || isLoading) && <Spinner size={theme.typography.body.fontSize} />}
{error && <Alert severity="error" title="Unable to retrieve descendant information" />}
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
breakdown: css({
...theme.typography.bodySmall,
color: theme.colors.text.secondary,
marginBottom: theme.spacing(2),
}),
});

View File

@ -1,6 +1,8 @@
import React, { useState } from 'react';
import { Space } from '@grafana/experimental';
import { Alert, Button, Field, Modal } from '@grafana/ui';
import { P } from '@grafana/ui/src/unstable';
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { DashboardTreeSelection } from '../../types';
@ -28,11 +30,17 @@ export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Pro
return (
<Modal title="Move" onDismiss={onDismiss} {...props}>
{selectedFolders.length > 0 && <Alert severity="warning" title="Moving this item may change its permissions." />}
This action will move the following content:
<P>This action will move the following content:</P>
<DescendantCount selectedItems={selectedItems} />
<Space v={3} />
<Field label="Folder name">
<FolderPicker allowEmpty onChange={({ uid }) => setMoveTarget(uid)} />
</Field>
<Modal.ButtonRow>
<Button onClick={onDismiss} variant="secondary" fill="outline">
Cancel

View File

@ -112,7 +112,7 @@ describe('browse-dashboards DashboardsTree', () => {
onAllSelectionChange={noop}
/>
);
expect(screen.queryByText('Empty folder')).toBeInTheDocument();
expect(screen.queryByText('No items')).toBeInTheDocument();
expect(screen.queryByText(emptyFolderIndicator.item.kind)).not.toBeInTheDocument();
});
});

View File

@ -204,9 +204,9 @@ const getStyles = (theme: GrafanaTheme2) => {
cell: css({
padding: theme.spacing(1),
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
overflow: 'hidden', // Required so flex children can do text-overflow: ellipsis
display: 'flex',
alignItems: 'center',
}),
link: css({

View File

@ -5,6 +5,7 @@ import { CellProps } from 'react-table';
import { GrafanaTheme2 } from '@grafana/data';
import { IconButton, Link, useStyles2 } from '@grafana/ui';
import { getSvgSize } from '@grafana/ui/src/components/Icon/utils';
import { Span, TextModifier } from '@grafana/ui/src/unstable';
import { DashboardsTreeItem } from '../types';
@ -23,7 +24,9 @@ export function NameCell({ row: { original: data }, onFolderClick }: NameCellPro
<>
<Indent level={level} />
<span className={styles.folderButtonSpacer} />
<em>Empty folder</em>
<em>
<TextModifier color="secondary">No items</TextModifier>
</em>
</>
);
}
@ -45,13 +48,15 @@ export function NameCell({ row: { original: data }, onFolderClick }: NameCellPro
<span className={styles.folderButtonSpacer} />
)}
{item.url ? (
<Link href={item.url} className={styles.link}>
{item.title}
</Link>
) : (
item.title
)}
<Span variant="body" truncate>
{item.url ? (
<Link href={item.url} className={styles.link}>
{item.title}
</Link>
) : (
item.title
)}
</Span>
</>
);
}

View File

@ -1,45 +1,35 @@
import { css } from '@emotion/css';
import React from 'react';
import { CellProps } from 'react-table';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, useStyles2 } from '@grafana/ui';
import { Icon } from '@grafana/ui';
import { TextModifier } from '@grafana/ui/src/unstable';
import { getIconForKind } from 'app/features/search/service/utils';
import { DashboardsTreeItem } from '../types';
export function TypeCell({ row: { original: data } }: CellProps<DashboardsTreeItem, unknown>) {
const styles = useStyles2(getStyles);
const iconName = getIconForKind(data.item.kind);
switch (data.item.kind) {
case 'dashboard':
return (
<span className={styles.text}>
<TextModifier color="secondary">
<Icon name={iconName} /> Dashboard
</span>
</TextModifier>
);
case 'folder':
return (
<span className={styles.text}>
<TextModifier color="secondary">
<Icon name={iconName} /> Folder
</span>
</TextModifier>
);
case 'panel':
return (
<span className={styles.text}>
<TextModifier color="secondary">
<Icon name={iconName} /> Panel
</span>
</TextModifier>
);
default:
return null;
}
}
function getStyles(theme: GrafanaTheme2) {
return {
text: css({
color: theme.colors.text.secondary,
}),
};
}