Dynamic dashboards: Warn if row or tab title not unique (#103346)

* warn if row or tab title not unique

* Make i18n-extract

* minor copy

* minor copy update again
pull/103466/head
Oscar Kilhed 2 months ago committed by GitHub
parent 3450d243b9
commit 17d075d81d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 44
      packages/grafana-ui/src/components/Tabs/Tab.tsx
  2. 6
      public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx
  3. 22
      public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx
  4. 7
      public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx
  5. 16
      public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx
  6. 6
      public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx
  7. 19
      public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx
  8. 11
      public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx
  9. 16
      public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx
  10. 8
      public/locales/en-US/grafana.json

@ -10,6 +10,7 @@ import { getFocusStyles } from '../../themes/mixins';
import { IconName } from '../../types';
import { clearButtonStyles } from '../Button';
import { Icon } from '../Icon/Icon';
import { Tooltip } from '../Tooltip/Tooltip';
import { Counter } from './Counter';
@ -25,10 +26,14 @@ export interface TabProps extends HTMLProps<HTMLElement> {
/** Extra content, displayed after the tab label and counter */
suffix?: NavModelItem['tabSuffix'];
truncate?: boolean;
tooltip?: string;
}
export const Tab = React.forwardRef<HTMLElement, TabProps>(
({ label, active, icon, onChangeTab, counter, suffix: Suffix, className, href, truncate, ...otherProps }, ref) => {
(
{ label, active, icon, onChangeTab, counter, suffix: Suffix, className, href, truncate, tooltip, ...otherProps },
ref
) => {
const tabsStyles = useStyles2(getStyles);
const clearStyles = useStyles2(clearButtonStyles);
@ -55,10 +60,13 @@ export const Tab = React.forwardRef<HTMLElement, TabProps>(
onClick: onChangeTab,
role: 'tab',
'aria-selected': active,
title: !!tooltip ? undefined : otherProps.title, // If tooltip is provided, don't set the title on the link or button, it looks weird
};
let tab = null;
if (href) {
return (
tab = (
<div className={cx(tabsStyles.item, truncate && tabsStyles.itemTruncate, className)}>
<a
{...commonProps}
@ -71,21 +79,27 @@ export const Tab = React.forwardRef<HTMLElement, TabProps>(
</a>
</div>
);
} else {
tab = (
<div className={cx(tabsStyles.item, truncate && tabsStyles.itemTruncate, className)}>
<button
{...commonProps}
type="button"
// don't think we can avoid the type assertion here :(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
ref={ref as React.ForwardedRef<HTMLButtonElement>}
>
{content()}
</button>
</div>
);
}
return (
<div className={cx(tabsStyles.item, truncate && tabsStyles.itemTruncate, className)}>
<button
{...commonProps}
type="button"
// don't think we can avoid the type assertion here :(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
ref={ref as React.ForwardedRef<HTMLButtonElement>}
>
{content()}
</button>
</div>
);
if (tooltip) {
return <Tooltip content={tooltip}>{tab}</Tooltip>;
}
return tab;
}
);

@ -210,4 +210,10 @@ export class RowItem
public setCollapsedState(collapse: boolean) {
this.setState({ collapse });
}
public hasUniqueTitle(): boolean {
const parentLayout = this.getParentLayout();
const duplicateTitles = parentLayout.duplicateTitles();
return !duplicateTitles.has(this.state.title);
}
}

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Alert, Input, Switch, TextLink } from '@grafana/ui';
import { Alert, Input, Switch, TextLink, Field } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
@ -81,14 +81,22 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
function RowTitleInput({ row }: { row: RowItem }) {
const { title, isNew } = row.useState();
const ref = useEditPaneInputAutoFocus({ autoFocus: isNew });
const hasUniqueTitle = row.hasUniqueTitle();
return (
<Input
ref={ref}
title={t('dashboard.rows-layout.row-options.title-option', 'Title')}
value={title}
onChange={(e) => row.onChangeTitle(e.currentTarget.value)}
/>
<Field
invalid={!hasUniqueTitle}
error={
!hasUniqueTitle ? t('dashboard.rows-layout.row-options.title-not-unique', 'Title should be unique') : undefined
}
>
<Input
ref={ref}
title={t('dashboard.rows-layout.row-options.title-option', 'Title')}
value={title}
onChange={(e) => row.onChangeTitle(e.currentTarget.value)}
/>
</Field>
);
}

@ -114,6 +114,13 @@ export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
)}
role="heading"
>
{!model.hasUniqueTitle() && (
<Tooltip
content={t('dashboard.rows-layout.row-warning.title-not-unique', 'This title is not unique')}
>
<Icon name="exclamation-triangle" />
</Tooltip>
)}
{title}
{isHeaderHidden && (
<Tooltip

@ -237,4 +237,20 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
return new RowsLayoutManager({ rows });
}
public duplicateTitles(): Set<string | undefined> {
const titleCounts = new Map<string | undefined, number>();
const duplicateTitles = new Set<string | undefined>();
this.state.rows.forEach((row) => {
const title = row.state.title;
const count = (titleCounts.get(title) ?? 0) + 1;
titleCounts.set(title, count);
if (count > 1 && title) {
duplicateTitles.add(title);
}
});
return duplicateTitles;
}
}

@ -180,4 +180,10 @@ export class TabItem
scrollCanvasElementIntoView(this, this.containerRef);
}
public hasUniqueTitle(): boolean {
const parentLayout = this.getParentLayout();
const duplicateTitles = parentLayout.duplicateTitles();
return !duplicateTitles.has(this.state.title);
}
}

@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { Input } from '@grafana/ui';
import { Input, Field } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
@ -44,6 +44,21 @@ export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[]
function TabTitleInput({ tab }: { tab: TabItem }) {
const { title, isNew } = tab.useState();
const ref = useEditPaneInputAutoFocus({ autoFocus: isNew });
const hasUniqueTitle = tab.hasUniqueTitle();
return <Input ref={ref} value={title} onChange={(e) => tab.onChangeTitle(e.currentTarget.value)} />;
return (
<Field
invalid={!hasUniqueTitle}
error={
!hasUniqueTitle ? t('dashboard.tabs-layout.tab-options.title-not-unique', 'Title should be unique') : undefined
}
>
<Input
ref={ref}
title={t('dashboard.tabs-layout.tab-options.title-option', 'Title')}
value={title}
onChange={(e) => tab.onChangeTitle(e.currentTarget.value)}
/>
</Field>
);
}

@ -5,6 +5,7 @@ import { useLocation } from 'react-router';
import { locationUtil, textUtil } from '@grafana/data';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { Tab, useElementSelection, usePointerDistance, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden';
import { useDashboardState } from '../../utils/utils';
@ -32,6 +33,15 @@ export function TabItemRenderer({ model }: SceneComponentProps<TabItem>) {
return null;
}
let titleCollisionProps = {};
if (!model.hasUniqueTitle()) {
titleCollisionProps = {
icon: 'exclamation-triangle',
tooltip: t('dashboard.tabs-layout.tab-warning.title-not-unique', 'This title is not unique'),
};
}
return (
<Draggable key={key!} draggableId={key!} index={myIndex} isDragDisabled={!isEditing}>
{(dragProvided, dragSnapshot) => (
@ -70,6 +80,7 @@ export function TabItemRenderer({ model }: SceneComponentProps<TabItem>) {
}}
label={titleInterpolated}
data-dashboard-drop-target-key={model.state.key}
{...titleCollisionProps}
/>
</div>
)}

@ -233,4 +233,20 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
return key;
}
public duplicateTitles() {
const titleCounts = new Map<string | undefined, number>();
const duplicateTitles = new Set<string | undefined>();
this.state.tabs.forEach((tab) => {
const title = tab.state.title;
const count = (titleCounts.get(title) ?? 0) + 1;
titleCounts.set(title, count);
if (count > 1) {
duplicateTitles.add(title);
}
});
return duplicateTitles;
}
}

@ -1821,7 +1821,11 @@
"hide-header": "Hide row header",
"title": "Title"
},
"title-not-unique": "Title should be unique",
"title-option": "Title"
},
"row-warning": {
"title-not-unique": "This title is not unique"
}
},
"save-dashboard-as-button": {
@ -1872,7 +1876,11 @@
"new": "New tab"
},
"tab-options": {
"title-not-unique": "Title should be unique",
"title-option": "Title"
},
"tab-warning": {
"title-not-unique": "This title is not unique"
}
},
"toolbar": {

Loading…
Cancel
Save