Datasource: datasource config page header redesign (#66999)

* Wip

* remove name input from page body

* render title & subTitle in datasource page in connections and remove unused prop - uid

* styling on actions button group

* remove unused props in edit datasource page

* set gap as 8px between buttons

* move editable datasourcename to the header

* add subTitle component

* remove useRef and use autoFocus prop

* pass false to isDefault  when dataSource is undefined

* change button text

* remove suffix icon

* remove unused import - Icon

* consolidate duplicate useDataSourceSettingsNav into datasrouces hook

* move dataSource context to useDataSourceSettingsNav

* remove Explore button in the footer

* remove unused props

* fix failing test on button group

* fix typo on file naming

* remove disabled prop

* fix param

* add test

* add test files

* disable editing title in readOnly provision datasource

* update name should save dataSource

* prevent swith toggle change from label clicking and change margin

* update tooltip message

* use datasource update on header instead of state update

* remvoe subTitle component and move subTitle component next to page Info component

* Added title

* remove subTitle in buildNavModel

* replace Button with Badge

* make datasourceheader as a component

* horizontal gap of 24px between pageInfo and actions components

* align page Info value items

* accept react node as page info label and add tooltip to Default item

* update navId for edit datasource page in connections

* update unit testing for Title

* fix gen_que

* betterer

* prettier fix

* fix e2e test

* add data-testid to nameEditIcon selector

* fix tooltip text

* fix navId for connections datasources edit page

* fix e2e selector: change autoSizeInput to Input

* adding ellipsis to EditDataSourceTitle

* override grafana-ui titleContainer h1 styles

* UI cleanup and apply readOnly to default datasource switch

* add period

* datasource name validation

* title and page info alignment

* add feature toggle - dataSourcePageHeader

* restore basicSettings component and apply feature toggle

* go lint

* Revert "title and page info alignment"

This reverts commit 681ac51f11.

* remove editable fields from page Header - name, default datasource switch

* fix go test: toggle generator

* remove test id

* remove alerting badge in BasicSettings component

* Revert "remove alerting badge in BasicSettings component"

This reverts commit fb33ff9028.

* feature toggle on alerting badge

* rename component & filename

* move DataSourceInfo type

* change button to link in test

---------

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
Co-authored-by: Miguel Palau Zarza <mpalauzarza@gmail.com>
This commit is contained in:
Taewoo K 2023-05-23 09:18:00 -04:00 committed by GitHub
parent 7f61cb92bc
commit 7f84e83ffe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 205 additions and 48 deletions

View File

@ -63,6 +63,7 @@ Some stable features are enabled by default. You can disable a stable feature by
| `enableElasticsearchBackendQuerying` | Enable the processing of queries and responses in the Elasticsearch data source through backend |
| `faroDatasourceSelector` | Enable the data source selector within the Frontend Apps section of the Frontend Observability |
| `enableDatagridEditing` | Enables the edit functionality in the datagrid panel |
| `dataSourcePageHeader` | Apply new pageHeader UI in data source edit page |
## Alpha feature toggles

View File

@ -95,5 +95,6 @@ export interface FeatureToggles {
advancedDataSourcePicker?: boolean;
faroDatasourceSelector?: boolean;
enableDatagridEditing?: boolean;
dataSourcePageHeader?: boolean;
extraThemes?: boolean;
}

View File

@ -24,4 +24,5 @@ const (
appO11ySquad codeowner = "@grafana/app-o11y"
grafanaPartnerPluginsSquad codeowner = "@grafana/partner-plugins"
grafanaOperatorExperienceSquad codeowner = "@grafana/grafana-operator-experience-squad"
enterpriseDatasourcesSquad codeowner = "@grafana/enterprise-datasources"
)

View File

@ -519,6 +519,13 @@ var (
State: FeatureStateBeta,
Owner: grafanaBiSquad,
},
{
Name: "dataSourcePageHeader",
Description: "Apply new pageHeader UI in data source edit page",
FrontendOnly: true,
State: FeatureStateBeta,
Owner: enterpriseDatasourcesSquad,
},
{
Name: "extraThemes",
Description: "Enables extra themes",

View File

@ -76,4 +76,5 @@ pluginsAPIManifestKey,alpha,@grafana/plugins-platform-backend,false,false,false,
advancedDataSourcePicker,stable,@grafana/dashboards-squad,false,false,false,true
faroDatasourceSelector,beta,@grafana/app-o11y,false,false,false,true
enableDatagridEditing,beta,@grafana/grafana-bi-squad,false,false,false,true
dataSourcePageHeader,beta,@grafana/enterprise-datasources,false,false,false,true
extraThemes,alpha,@grafana/user-essentials,false,false,false,true

1 Name State Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
76 advancedDataSourcePicker stable @grafana/dashboards-squad false false false true
77 faroDatasourceSelector beta @grafana/app-o11y false false false true
78 enableDatagridEditing beta @grafana/grafana-bi-squad false false false true
79 dataSourcePageHeader beta @grafana/enterprise-datasources false false false true
80 extraThemes alpha @grafana/user-essentials false false false true

View File

@ -315,6 +315,10 @@ const (
// Enables the edit functionality in the datagrid panel
FlagEnableDatagridEditing = "enableDatagridEditing"
// FlagDataSourcePageHeader
// Apply new pageHeader UI in data source edit page
FlagDataSourcePageHeader = "dataSourcePageHeader"
// FlagExtraThemes
// Enables extra themes
FlagExtraThemes = "extraThemes"

View File

@ -46,7 +46,7 @@ const getStyles = (theme: GrafanaTheme2) => {
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
gap: theme.spacing(1, 2),
gap: theme.spacing(1, 3),
}),
title: css({
display: 'flex',

View File

@ -3,15 +3,16 @@ import { useParams } from 'react-router-dom';
import { Page } from 'app/core/components/Page/Page';
import { DataSourceDashboards } from 'app/features/datasources/components/DataSourceDashboards';
import { useDataSourceSettingsNav } from '../hooks/useDataSourceSettingsNav';
import { useDataSourceSettingsNav } from 'app/features/datasources/state';
export function DataSourceDashboardsPage() {
const { uid } = useParams<{ uid: string }>();
const { navId, pageNav } = useDataSourceSettingsNav('dashboards');
const params = new URLSearchParams(location.search);
const pageId = params.get('page');
const nav = useDataSourceSettingsNav(uid, pageId);
return (
<Page navId={navId} pageNav={pageNav}>
<Page navId="connections-your-connections-datasources" pageNav={nav.main}>
<Page.Contents>
<DataSourceDashboards uid={uid} />
</Page.Contents>

View File

@ -1,7 +1,9 @@
import * as React from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { config } from '@grafana/runtime';
import { Page } from 'app/core/components/Page/Page';
import DataSourceTabPage from 'app/features/datasources/components/DataSourceTabPage';
import { EditDataSource } from 'app/features/datasources/components/EditDataSource';
import { EditDataSourceActions } from 'app/features/datasources/components/EditDataSourceActions';
@ -12,8 +14,13 @@ export function EditDataSourcePage() {
const location = useLocation();
const params = new URLSearchParams(location.search);
const pageId = params.get('page');
const dataSourcePageHeader = config.featureToggles.dataSourcePageHeader;
const { navId, pageNav } = useDataSourceSettingsNav();
if (dataSourcePageHeader) {
return <DataSourceTabPage uid={uid} pageId={pageId} navId="connections-datasources" />;
}
return (
<Page navId={navId} pageNav={pageNav} actions={<EditDataSourceActions uid={uid} />}>
<Page.Contents>

View File

@ -3,6 +3,7 @@ import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { InlineField, InlineSwitch, Input, Badge, useStyles2 } from '@grafana/ui';
export interface Props {
@ -24,7 +25,7 @@ export function BasicSettings({
}: Props) {
return (
<>
<AlertingEnabled enabled={alertingSupported} />
{!config.featureToggles.dataSourcePageHeader && <AlertingEnabled enabled={alertingSupported} />}
<div className="gf-form-group" aria-label="Datasource settings page basic settings">
<div className="gf-form-inline">

View File

@ -26,6 +26,7 @@ describe('<ButtonRow>', () => {
expect(screen.getByTestId(selectors.pages.DataSource.delete)).toBeInTheDocument();
expect(screen.getByText('Test')).toBeInTheDocument();
});
it('should render save & test', () => {
setup({ canSave: true });

View File

@ -0,0 +1,41 @@
import React from 'react';
import { Page } from 'app/core/components/Page/Page';
import { EditDataSource } from '../components/EditDataSource';
import { EditDataSourceActions } from '../components/EditDataSourceActions';
import { useDataSourceInfo } from '../components/useDataSourceInfo';
import { useDataSourceSettingsNav } from '../state';
import { DataSourceTitle } from './DataSourceTitle';
export interface Props {
uid: string;
pageId: string | null;
navId: string;
}
export function DataSourceTabPage({ uid, pageId, navId }: Props) {
const nav = useDataSourceSettingsNav(uid, pageId);
const info = useDataSourceInfo({
dataSourcePluginName: nav.main.dataSourcePluginName,
alertingSupported: nav.dataSourceHeader.alertingSupported,
});
return (
<Page
navId={navId}
pageNav={nav.main}
renderTitle={(title) => <DataSourceTitle title={title} />}
info={info}
actions={<EditDataSourceActions uid={uid} />}
>
<Page.Contents>
<EditDataSource uid={uid} pageId={pageId} />
</Page.Contents>
</Page>
);
}
export default DataSourceTabPage;

View File

@ -0,0 +1,38 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
interface Props {
title: string;
}
export function DataSourceTitle({ title }: Props) {
const styles = useStyles2(getStyles);
return (
<div className={styles.container}>
<h1 className={styles.title}>{title}</h1>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
marginBottom: theme.spacing(2),
h1: {
display: 'inline-block',
},
}),
title: css({
display: 'inline-block',
margin: '0 0 0 0',
maxWidth: '40vw',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}),
};
};

View File

@ -1,9 +1,7 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { LinkButton, useStyles2 } from '@grafana/ui';
import { LinkButton } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types';
@ -11,28 +9,35 @@ import { useDataSource } from '../state';
import { trackCreateDashboardClicked, trackExploreClicked } from '../tracking';
import { constructDataSourceExploreUrl } from '../utils';
const getStyles = (theme: GrafanaTheme2) => {
return {
button: css({
marginLeft: theme.spacing(2),
}),
};
};
interface Props {
uid: string;
}
export function EditDataSourceActions({ uid }: Props) {
const styles = useStyles2(getStyles);
const dataSource = useDataSource(uid);
const hasExploreRights = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
return (
<>
{hasExploreRights && (
<LinkButton
variant="secondary"
size="sm"
href={constructDataSourceExploreUrl(dataSource)}
onClick={() => {
trackExploreClicked({
grafana_version: config.buildInfo.version,
datasource_uid: dataSource.uid,
plugin_name: dataSource.typeName,
path: location.pathname,
});
}}
>
Explore data
</LinkButton>
)}
<LinkButton
icon="apps"
fill="outline"
size="sm"
variant="secondary"
href={`dashboard/new-with-ds/${dataSource.uid}`}
onClick={() => {
@ -46,26 +51,6 @@ export function EditDataSourceActions({ uid }: Props) {
>
Build a dashboard
</LinkButton>
{hasExploreRights && (
<LinkButton
icon="compass"
fill="outline"
variant="secondary"
className={styles.button}
href={constructDataSourceExploreUrl(dataSource)}
onClick={() => {
trackExploreClicked({
grafana_version: config.buildInfo.version,
datasource_uid: dataSource.uid,
plugin_name: dataSource.typeName,
path: location.pathname,
});
}}
>
Explore
</LinkButton>
)}
</>
);
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Badge } from '@grafana/ui';
import { PageInfoItem } from 'app/core/components/Page/types';
type DataSourceInfo = {
dataSourcePluginName: string;
alertingSupported: boolean;
};
export const useDataSourceInfo = (dataSourceInfo: DataSourceInfo): PageInfoItem[] => {
const info: PageInfoItem[] = [];
const alertingEnabled = dataSourceInfo.alertingSupported;
info.push({
label: 'Type',
value: dataSourceInfo.dataSourcePluginName,
});
info.push({
label: 'Alerting',
value: (
<Badge color={alertingEnabled ? 'green' : 'red'} text={alertingEnabled ? 'Supported' : 'Not supported'}></Badge>
),
});
return info;
};

View File

@ -116,9 +116,8 @@ describe('<EditDataSourcePage>', () => {
await waitFor(() => {
// Buttons
expect(screen.queryByRole('button', { name: /Delete/i })).toBeVisible();
expect(screen.queryByRole('button', { name: /Save (.*) test/i })).toBeVisible();
expect(screen.queryByRole('link', { name: /Build a dashboard/i })).toBeVisible();
expect(screen.queryAllByRole('link', { name: /Explore/i })).toHaveLength(1);
expect(screen.queryAllByRole('link', { name: /Explore data/i })).toHaveLength(1);
});
});
});

View File

@ -1,8 +1,10 @@
import React from 'react';
import { config } from '@grafana/runtime';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import DataSourceTabPage from '../components/DataSourceTabPage';
import { EditDataSource } from '../components/EditDataSource';
import { EditDataSourceActions } from '../components/EditDataSourceActions';
import { useDataSourceSettingsNav } from '../state';
@ -13,8 +15,13 @@ export function EditDataSourcePage(props: Props) {
const uid = props.match.params.uid;
const params = new URLSearchParams(props.location.search);
const pageId = params.get('page');
const dataSourcePageHeader = config.featureToggles.dataSourcePageHeader;
const nav = useDataSourceSettingsNav(uid, pageId);
if (dataSourcePageHeader) {
return <DataSourceTabPage uid={uid} pageId={pageId} navId="datasources" />;
}
return (
<Page navId="datasources" pageNav={nav.main} actions={<EditDataSourceActions uid={uid} />}>
<Page.Contents>

View File

@ -1,10 +1,12 @@
import { useContext, useEffect } from 'react';
import { DataSourcePluginMeta, DataSourceSettings, NavModelItem } from '@grafana/data';
import { DataSourcePluginMeta, DataSourceSettings, NavModel, NavModelItem } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { cleanUpAction } from 'app/core/actions/cleanUp';
import appEvents from 'app/core/app_events';
import { contextSrv } from 'app/core/core';
import { getNavModel } from 'app/core/selectors/navModel';
import { useGetSingle } from 'app/features/plugins/admin/state/hooks';
import { AccessControlAction, useDispatch, useSelector } from 'app/types';
import { ShowConfirmModalEvent } from 'app/types/events';
@ -127,10 +129,24 @@ export const useDataSourceSettings = () => {
};
export const useDataSourceSettingsNav = (dataSourceId: string, pageId: string | null) => {
const dataSource = useDataSource(dataSourceId);
const { plugin, loadError, loading } = useDataSourceSettings();
const dataSource = useDataSource(dataSourceId);
const dsi = getDataSourceSrv()?.getInstanceSettings(dataSourceId);
const hasAlertingEnabled = Boolean(dsi?.meta?.alerting ?? false);
const isAlertManagerDatasource = dsi?.type === 'alertmanager';
const alertingSupported = hasAlertingEnabled || isAlertManagerDatasource;
const datasourcePlugin = useGetSingle(dataSource.type);
const navIndex = useSelector((state) => state.navIndex);
const navIndexId = pageId ? `datasource-${pageId}-${dataSourceId}` : `datasource-settings-${dataSourceId}`;
let pageNav: NavModel = {
node: {
text: 'Data Source Nav Node',
},
main: {
text: 'Data Source Nav Node',
},
};
if (loadError) {
const node: NavModelItem = {
@ -139,17 +155,36 @@ export const useDataSourceSettingsNav = (dataSourceId: string, pageId: string |
icon: 'exclamation-triangle',
};
return {
pageNav = {
node: node,
main: node,
};
}
if (loading || !plugin) {
return getNavModel(navIndex, navIndexId, getDataSourceLoadingNav('settings'));
pageNav = getNavModel(navIndex, navIndexId, getDataSourceLoadingNav('settings'));
}
return getNavModel(navIndex, navIndexId, getDataSourceNav(buildNavModel(dataSource, plugin), pageId || 'settings'));
if (plugin) {
pageNav = getNavModel(
navIndex,
navIndexId,
getDataSourceNav(buildNavModel(dataSource, plugin), pageId || 'settings')
);
}
return {
node: pageNav.node,
main: {
...pageNav.main,
text: dataSource.name,
dataSourcePluginName: datasourcePlugin?.name || '',
active: true,
},
dataSourceHeader: {
alertingSupported,
},
};
};
export const useDataSourceRights = (uid: string): DataSourceRights => {

View File

@ -16,7 +16,6 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
const navModel: NavModelItem = {
img: pluginMeta.info.logos.large,
id: 'datasource-' + dataSource.uid,
subTitle: `Type: ${pluginMeta.name}`,
url: '',
text: dataSource.name,
children: [