PublicDashboards: UI improvements (#55130)

* Public dashboard modal UI modifications
This commit is contained in:
juanicabanas
2022-09-14 14:35:46 -03:00
committed by GitHub
parent 29327cbba2
commit 1e06b0170b
6 changed files with 184 additions and 141 deletions

View File

@@ -181,7 +181,7 @@ export const Pages = {
ShareDashboardModal: { ShareDashboardModal: {
shareButton: 'Share dashboard or panel', shareButton: 'Share dashboard or panel',
PublicDashboard: { PublicDashboard: {
Tab: 'Tab Public Dashboard', Tab: 'Tab Public dashboard',
WillBePublicCheckbox: 'data-testid public dashboard will be public checkbox', WillBePublicCheckbox: 'data-testid public dashboard will be public checkbox',
LimitedDSCheckbox: 'data-testid public dashboard limited datasources checkbox', LimitedDSCheckbox: 'data-testid public dashboard limited datasources checkbox',
CostIncreaseCheckbox: 'data-testid public dashboard cost may increase checkbox', CostIncreaseCheckbox: 'data-testid public dashboard cost may increase checkbox',

View File

@@ -62,7 +62,7 @@ function getTabs(props: Props) {
} }
if (Boolean(config.featureToggles['publicDashboards'])) { if (Boolean(config.featureToggles['publicDashboards'])) {
tabs.push({ label: 'Public Dashboard', value: 'share', component: SharePublicDashboard }); tabs.push({ label: 'Public dashboard', value: 'share', component: SharePublicDashboard });
} }
return tabs; return tabs;

View File

@@ -74,7 +74,7 @@ describe('SharePublic', () => {
render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />); render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />);
expect(screen.getByRole('tablist')).toHaveTextContent('Link'); expect(screen.getByRole('tablist')).toHaveTextContent('Link');
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public Dashboard'); expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard');
}); });
it('renders share panel when public dashboards feature is enabled', async () => { it('renders share panel when public dashboards feature is enabled', async () => {
@@ -90,14 +90,14 @@ describe('SharePublic', () => {
await waitFor(() => screen.getByText('Link')); await waitFor(() => screen.getByText('Link'));
expect(screen.getByRole('tablist')).toHaveTextContent('Link'); expect(screen.getByRole('tablist')).toHaveTextContent('Link');
expect(screen.getByRole('tablist')).toHaveTextContent('Public Dashboard'); expect(screen.getByRole('tablist')).toHaveTextContent('Public dashboard');
fireEvent.click(screen.getByText('Public Dashboard')); fireEvent.click(screen.getByText('Public dashboard'));
await screen.findByText('Welcome to Grafana public dashboards alpha!'); await screen.findByText('Welcome to Grafana public dashboards alpha!');
}); });
it('renders default time in inputs', async () => { it('renders default relative time in input', async () => {
config.featureToggles.publicDashboards = true; config.featureToggles.publicDashboards = true;
const mockDashboard = new DashboardModel({ const mockDashboard = new DashboardModel({
uid: 'mockDashboardUid', uid: 'mockDashboardUid',
@@ -107,17 +107,38 @@ describe('SharePublic', () => {
}); });
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' }); expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
//@ts-ignore //@ts-ignore
mockDashboard.originalTime = { from: 'test-from', to: 'test-to' }; mockDashboard.originalTime = { from: 'now-6h', to: 'now' };
render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />); render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />);
await waitFor(() => screen.getByText('Link')); await waitFor(() => screen.getByText('Link'));
fireEvent.click(screen.getByText('Public Dashboard')); fireEvent.click(screen.getByText('Public dashboard'));
await screen.findByText('Welcome to Grafana public dashboards alpha!'); await screen.findByText('Welcome to Grafana public dashboards alpha!');
expect(screen.getByDisplayValue('test-from')).toBeInTheDocument(); expect(screen.getByText('Last 6 hours')).toBeInTheDocument();
expect(screen.getByDisplayValue('test-to')).toBeInTheDocument(); });
it('renders default absolute time in input 2', async () => {
config.featureToggles.publicDashboards = true;
const mockDashboard = new DashboardModel({
uid: 'mockDashboardUid',
});
const mockPanel = new PanelModel({
id: 'mockPanelId',
});
mockDashboard.time = { from: '2022-08-30T03:00:00.000Z', to: '2022-09-04T02:59:59.000Z' };
//@ts-ignore
mockDashboard.originalTime = { from: '2022-08-30T06:00:00.000Z', to: '2022-09-04T06:59:59.000Z' };
render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />);
await waitFor(() => screen.getByText('Link'));
fireEvent.click(screen.getByText('Public dashboard'));
await screen.findByText('Welcome to Grafana public dashboards alpha!');
expect(screen.getByText('2022-08-30 00:00:00 to 2022-09-04 01:59:59')).toBeInTheDocument();
}); });
// test checking if current version of dashboard in state is persisted to db // test checking if current version of dashboard in state is persisted to db

View File

@@ -1,5 +1,7 @@
import { css } from '@emotion/css';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime/src'; import { reportInteraction } from '@grafana/runtime/src';
import { import {
@@ -8,14 +10,19 @@ import {
Checkbox, Checkbox,
ClipboardButton, ClipboardButton,
Field, Field,
HorizontalGroup,
FieldSet, FieldSet,
Input, Input,
Label, Label,
LinkButton, LinkButton,
Switch, Switch,
TimeRangeInput,
useStyles2,
VerticalGroup,
} from '@grafana/ui'; } from '@grafana/ui';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification'; import { createErrorNotification } from 'app/core/copy/appNotification';
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
import { dispatch } from 'app/store/store'; import { dispatch } from 'app/store/store';
import { contextSrv } from '../../../../core/services/context_srv'; import { contextSrv } from '../../../../core/services/context_srv';
@@ -43,6 +50,7 @@ interface Acknowledgements {
export const SharePublicDashboard = (props: Props) => { export const SharePublicDashboard = (props: Props) => {
const dashboardVariables = props.dashboard.getVariables(); const dashboardVariables = props.dashboard.getVariables();
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard; const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
const styles = useStyles2(getStyles);
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin()); const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
@@ -57,6 +65,11 @@ export const SharePublicDashboard = (props: Props) => {
usage: false, usage: false,
}); });
const timeRange = getTimeRange(
{ from: props.dashboard.getDefaultTime().from, to: props.dashboard.getDefaultTime().to },
props.dashboard.timezone
);
useEffect(() => { useEffect(() => {
reportInteraction('grafana_dashboards_public_share_viewed'); reportInteraction('grafana_dashboards_public_share_viewed');
@@ -94,9 +107,7 @@ export const SharePublicDashboard = (props: Props) => {
); );
// check if all conditions have been acknowledged // check if all conditions have been acknowledged
const acknowledged = () => { const acknowledged = acknowledgements.public && acknowledgements.datasources && acknowledgements.usage;
return acknowledgements.public && acknowledgements.datasources && acknowledgements.usage;
};
return ( return (
<> <>
@@ -115,6 +126,7 @@ export const SharePublicDashboard = (props: Props) => {
To allow the current dashboard to be published publicly, toggle the switch. For now we do not support To allow the current dashboard to be published publicly, toggle the switch. For now we do not support
template variables or frontend datasources. template variables or frontend datasources.
</p> </p>
<p>
We&apos;d love your feedback. To share, please comment on this{' '} We&apos;d love your feedback. To share, please comment on this{' '}
<a <a
href="https://github.com/grafana/grafana/discussions/49253" href="https://github.com/grafana/grafana/discussions/49253"
@@ -125,21 +137,19 @@ export const SharePublicDashboard = (props: Props) => {
GitHub discussion GitHub discussion
</a> </a>
. .
</p>
<hr /> <hr />
<div> <div className={styles.checkboxes}>
Before you click Save, please acknowledge the following information: <br /> <p>Before you click Save, please acknowledge the following information:</p>
<FieldSet disabled={publicDashboardPersisted(publicDashboard) || !hasWritePermissions}> <FieldSet disabled={publicDashboardPersisted(publicDashboard) || !hasWritePermissions}>
<br /> <VerticalGroup spacing="md">
<div>
<Checkbox <Checkbox
label="Your entire dashboard will be public" label="Your entire dashboard will be public"
value={acknowledgements.public} value={acknowledgements.public}
data-testid={selectors.WillBePublicCheckbox} data-testid={selectors.WillBePublicCheckbox}
onChange={(e) => onAcknowledge('public', e.currentTarget.checked)} onChange={(e) => onAcknowledge('public', e.currentTarget.checked)}
/> />
</div> <HorizontalGroup spacing="none">
<br />
<div>
<Checkbox <Checkbox
label="Publishing currently only works with a subset of datasources" label="Publishing currently only works with a subset of datasources"
value={acknowledgements.datasources} value={acknowledgements.datasources}
@@ -155,8 +165,8 @@ export const SharePublicDashboard = (props: Props) => {
rel="noopener noreferrer" rel="noopener noreferrer"
tooltip="Learn more about public datasources" tooltip="Learn more about public datasources"
/> />
</div> </HorizontalGroup>
<br /> <HorizontalGroup spacing="none">
<Checkbox <Checkbox
label="Making your dashboard public will cause queries to run each time the dashboard is viewed which may increase costs" label="Making your dashboard public will cause queries to run each time the dashboard is viewed which may increase costs"
value={acknowledgements.usage} value={acknowledgements.usage}
@@ -172,34 +182,23 @@ export const SharePublicDashboard = (props: Props) => {
rel="noopener noreferrer" rel="noopener noreferrer"
tooltip="Learn more about query caching" tooltip="Learn more about query caching"
/> />
<br /> </HorizontalGroup>
<br /> </VerticalGroup>
</FieldSet> </FieldSet>
</div> </div>
<hr />
<div> <div>
<h4 className="share-modal-info-text">Public Dashboard Configuration</h4> <h4 className="share-modal-info-text">Public dashboard configuration</h4>
<FieldSet disabled={!hasWritePermissions}> <FieldSet disabled={!hasWritePermissions} className={styles.dashboardConfig}>
<VerticalGroup spacing="md">
<HorizontalGroup spacing="xs" justify="space-between">
<Label description="The public dashboard uses the default time settings of the dashboard"> <Label description="The public dashboard uses the default time settings of the dashboard">
Time Range Time Range
</Label> </Label>
<div style={{ padding: '5px' }}> <TimeRangeInput value={timeRange} disabled onChange={() => {}} />
<Input </HorizontalGroup>
value={props.dashboard.getDefaultTime().from} <HorizontalGroup spacing="xs" justify="space-between">
disabled={true} <Label description="Configures whether current dashboard can be available publicly">Enabled</Label>
addonBefore={
<span style={{ width: '50px', display: 'flex', alignItems: 'center', padding: '5px' }}>From:</span>
}
/>
<Input
value={props.dashboard.getDefaultTime().to}
disabled={true}
addonBefore={
<span style={{ width: '50px', display: 'flex', alignItems: 'center', padding: '5px' }}>To:</span>
}
/>
</div>
<br />
<Field label="Enabled" description="Configures whether current dashboard can be available publicly">
<Switch <Switch
disabled={dashboardHasTemplateVariables(dashboardVariables)} disabled={dashboardHasTemplateVariables(dashboardVariables)}
data-testid={selectors.EnableSwitch} data-testid={selectors.EnableSwitch}
@@ -215,12 +214,9 @@ export const SharePublicDashboard = (props: Props) => {
}); });
}} }}
/> />
</Field> </HorizontalGroup>
</FieldSet>
<FieldSet>
{publicDashboardPersisted(publicDashboard) && publicDashboard.isEnabled && ( {publicDashboardPersisted(publicDashboard) && publicDashboard.isEnabled && (
<Field label="Link URL"> <Field label="Link URL" className={styles.publicUrl}>
<Input <Input
value={generatePublicDashboardUrl(publicDashboard)} value={generatePublicDashboardUrl(publicDashboard)}
readOnly readOnly
@@ -230,9 +226,7 @@ export const SharePublicDashboard = (props: Props) => {
data-testid={selectors.CopyUrlButton} data-testid={selectors.CopyUrlButton}
variant="primary" variant="primary"
icon="copy" icon="copy"
getText={() => { getText={() => generatePublicDashboardUrl(publicDashboard)}
return generatePublicDashboardUrl(publicDashboard);
}}
> >
Copy Copy
</ClipboardButton> </ClipboardButton>
@@ -240,8 +234,8 @@ export const SharePublicDashboard = (props: Props) => {
/> />
</Field> </Field>
)} )}
</VerticalGroup>
</FieldSet> </FieldSet>
{hasWritePermissions ? ( {hasWritePermissions ? (
props.dashboard.hasUnsavedChanges() && ( props.dashboard.hasUnsavedChanges() && (
<Alert <Alert
@@ -253,11 +247,11 @@ export const SharePublicDashboard = (props: Props) => {
<Alert title="You don't have permissions to create or update a public dashboard" severity="warning" /> <Alert title="You don't have permissions to create or update a public dashboard" severity="warning" />
)} )}
<Button <Button
disabled={!hasWritePermissions || !acknowledged() || props.dashboard.hasUnsavedChanges()} disabled={!hasWritePermissions || !acknowledged || props.dashboard.hasUnsavedChanges()}
onClick={onSavePublicConfig} onClick={onSavePublicConfig}
data-testid={selectors.SaveConfigButton} data-testid={selectors.SaveConfigButton}
> >
Save Sharing Configuration Save sharing configuration
</Button> </Button>
</div> </div>
</> </>
@@ -265,3 +259,20 @@ export const SharePublicDashboard = (props: Props) => {
</> </>
); );
}; };
const getStyles = (theme: GrafanaTheme2) => ({
checkboxes: css`
margin: ${theme.spacing(2, 0)};
`,
timeRange: css`
padding: ${theme.spacing(1, 1)};
margin: ${theme.spacing(0, 0, 2, 0)};
`,
dashboardConfig: css`
margin: ${theme.spacing(0, 0, 3, 0)};
`,
publicUrl: css`
width: 100%;
margin-bottom: 0;
`,
});

View File

@@ -15,6 +15,7 @@ import appEvents from 'app/core/app_events';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { contextSrv, ContextSrv } from 'app/core/services/context_srv'; import { contextSrv, ContextSrv } from 'app/core/services/context_srv';
import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker'; import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker';
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
import { AbsoluteTimeEvent, ShiftTimeEvent, ShiftTimeEventDirection, ZoomOutEvent } from '../../../types/events'; import { AbsoluteTimeEvent, ShiftTimeEvent, ShiftTimeEventDirection, ZoomOutEvent } from '../../../types/events';
import { TimeModel } from '../state/TimeModel'; import { TimeModel } from '../state/TimeModel';
@@ -316,19 +317,7 @@ export class TimeSrv {
}; };
timeRange(): TimeRange { timeRange(): TimeRange {
// make copies if they are moment (do not want to return out internal moment, because they are mutable!) return getTimeRange(this.time, this.timeModel);
const raw = {
from: isDateTime(this.time.from) ? dateTime(this.time.from) : this.time.from,
to: isDateTime(this.time.to) ? dateTime(this.time.to) : this.time.to,
};
const timezone = this.timeModel ? this.timeModel.getTimezone() : undefined;
return {
from: dateMath.parse(raw.from, false, timezone, this.timeModel?.fiscalYearStartMonth)!,
to: dateMath.parse(raw.to, true, timezone, this.timeModel?.fiscalYearStartMonth)!,
raw: raw,
};
} }
zoomOut(factor: number, updateUrl = true) { zoomOut(factor: number, updateUrl = true) {

View File

@@ -0,0 +1,22 @@
import { DateTime, TimeRange } from '@grafana/data';
import { dateMath, dateTime, isDateTime } from '@grafana/data/src';
import { TimeModel } from 'app/features/dashboard/state/TimeModel';
export const getTimeRange = (
time: { from: DateTime | string; to: DateTime | string },
timeModel?: TimeModel
): TimeRange => {
// make copies if they are moment (do not want to return out internal moment, because they are mutable!)
const raw = {
from: isDateTime(time.from) ? dateTime(time.from) : time.from,
to: isDateTime(time.to) ? dateTime(time.to) : time.to,
};
const timezone = timeModel ? timeModel.getTimezone() : undefined;
return {
from: dateMath.parse(raw.from, false, timezone, timeModel?.fiscalYearStartMonth)!,
to: dateMath.parse(raw.to, true, timezone, timeModel?.fiscalYearStartMonth)!,
raw: raw,
};
};