mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PublicDashboard: Add RTK Query with loading and error state. Add MSW dependency for testing. (#55518)
PublicDashboard: Add RTK Query with loading and error state. Add MSW dependency for testing.
This commit is contained in:
@@ -5,6 +5,7 @@ import { reportInteraction } from '@grafana/runtime/src';
|
||||
import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { SharePublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard';
|
||||
|
||||
@@ -12,7 +13,6 @@ import { ShareEmbed } from './ShareEmbed';
|
||||
import { ShareExport } from './ShareExport';
|
||||
import { ShareLibraryPanel } from './ShareLibraryPanel';
|
||||
import { ShareLink } from './ShareLink';
|
||||
import { SharePublicDashboard } from './SharePublicDashboard';
|
||||
import { ShareSnapshot } from './ShareSnapshot';
|
||||
import { ShareModalTabModel } from './types';
|
||||
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { BootData } from '@grafana/data';
|
||||
import { BackendSrv, setEchoSrv } from '@grafana/runtime';
|
||||
import config from 'app/core/config';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
|
||||
import { Echo } from '../../../../core/services/echo/Echo';
|
||||
|
||||
import { ShareModal } from './ShareModal';
|
||||
import { PublicDashboard } from './SharePublicDashboardUtils';
|
||||
|
||||
// Mock api request
|
||||
const publicDashboardconfigResp: PublicDashboard = {
|
||||
isEnabled: true,
|
||||
uid: '',
|
||||
dashboardUid: '',
|
||||
accessToken: '',
|
||||
};
|
||||
|
||||
const backendSrv = {
|
||||
get: () => publicDashboardconfigResp,
|
||||
} as unknown as BackendSrv;
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
|
||||
jest.mock('app/core/core', () => {
|
||||
return {
|
||||
contextSrv: {
|
||||
hasPermission: () => true,
|
||||
},
|
||||
appEvents: {
|
||||
subscribe: () => {
|
||||
return {
|
||||
unsubscribe: () => {},
|
||||
};
|
||||
},
|
||||
emit: () => {},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('SharePublic', () => {
|
||||
let originalBootData: BootData;
|
||||
|
||||
beforeAll(() => {
|
||||
setEchoSrv(new Echo());
|
||||
originalBootData = config.bootData;
|
||||
config.appUrl = 'http://dashboards.grafana.com/';
|
||||
|
||||
config.bootData = {
|
||||
user: {
|
||||
orgId: 1,
|
||||
},
|
||||
} as any;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.bootData = originalBootData;
|
||||
});
|
||||
|
||||
it('does not render share panel when public dashboards feature is disabled', () => {
|
||||
const mockDashboard = new DashboardModel({
|
||||
uid: 'mockDashboardUid',
|
||||
});
|
||||
const mockPanel = new PanelModel({
|
||||
id: 'mockPanelId',
|
||||
});
|
||||
|
||||
render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />);
|
||||
|
||||
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
||||
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard');
|
||||
});
|
||||
|
||||
it('renders share panel when public dashboards feature is enabled', async () => {
|
||||
config.featureToggles.publicDashboards = true;
|
||||
const mockDashboard = new DashboardModel({
|
||||
uid: 'mockDashboardUid',
|
||||
});
|
||||
const mockPanel = new PanelModel({
|
||||
id: 'mockPanelId',
|
||||
});
|
||||
|
||||
render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />);
|
||||
|
||||
await waitFor(() => screen.getByText('Link'));
|
||||
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
||||
expect(screen.getByRole('tablist')).toHaveTextContent('Public dashboard');
|
||||
|
||||
fireEvent.click(screen.getByText('Public dashboard'));
|
||||
|
||||
await screen.findByText('Welcome to Grafana public dashboards alpha!');
|
||||
});
|
||||
|
||||
it('renders default relative time in input', async () => {
|
||||
config.featureToggles.publicDashboards = true;
|
||||
const mockDashboard = new DashboardModel({
|
||||
uid: 'mockDashboardUid',
|
||||
});
|
||||
const mockPanel = new PanelModel({
|
||||
id: 'mockPanelId',
|
||||
});
|
||||
|
||||
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
|
||||
|
||||
//@ts-ignore
|
||||
mockDashboard.originalTime = { from: 'now-6h', to: 'now' };
|
||||
|
||||
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('Last 6 hours')).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
|
||||
});
|
||||
@@ -1,286 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||
import { reportInteraction } from '@grafana/runtime/src';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
ClipboardButton,
|
||||
Field,
|
||||
HorizontalGroup,
|
||||
FieldSet,
|
||||
Input,
|
||||
Label,
|
||||
LinkButton,
|
||||
Switch,
|
||||
TimeRangeInput,
|
||||
useStyles2,
|
||||
VerticalGroup,
|
||||
} from '@grafana/ui';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
||||
import { contextSrv } from '../../../../core/services/context_srv';
|
||||
import { AccessControlAction } from '../../../../types';
|
||||
import { isOrgAdmin } from '../../../plugins/admin/permissions';
|
||||
|
||||
import {
|
||||
dashboardHasTemplateVariables,
|
||||
generatePublicDashboardUrl,
|
||||
getPublicDashboardConfig,
|
||||
PublicDashboard,
|
||||
publicDashboardPersisted,
|
||||
savePublicDashboardConfig,
|
||||
} from './SharePublicDashboardUtils';
|
||||
import { ShareModalTabProps } from './types';
|
||||
|
||||
interface Props extends ShareModalTabProps {}
|
||||
|
||||
interface Acknowledgements {
|
||||
public: boolean;
|
||||
datasources: boolean;
|
||||
usage: boolean;
|
||||
}
|
||||
|
||||
export const SharePublicDashboard = (props: Props) => {
|
||||
const dashboardVariables = props.dashboard.getVariables();
|
||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
|
||||
|
||||
const [publicDashboard, setPublicDashboardConfig] = useState<PublicDashboard>({
|
||||
isEnabled: false,
|
||||
uid: '',
|
||||
dashboardUid: props.dashboard.uid,
|
||||
});
|
||||
const [acknowledgements, setAcknowledgements] = useState<Acknowledgements>({
|
||||
public: false,
|
||||
datasources: false,
|
||||
usage: false,
|
||||
});
|
||||
|
||||
const timeRange = getTimeRange(props.dashboard.getDefaultTime(), props.dashboard);
|
||||
|
||||
useEffect(() => {
|
||||
reportInteraction('grafana_dashboards_public_share_viewed');
|
||||
|
||||
getPublicDashboardConfig(props.dashboard.uid, setPublicDashboardConfig).catch();
|
||||
}, [props.dashboard.uid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (publicDashboardPersisted(publicDashboard)) {
|
||||
setAcknowledgements({
|
||||
public: true,
|
||||
datasources: true,
|
||||
usage: true,
|
||||
});
|
||||
}
|
||||
}, [publicDashboard]);
|
||||
|
||||
const onSavePublicConfig = () => {
|
||||
reportInteraction('grafana_dashboards_public_create_clicked');
|
||||
|
||||
if (dashboardHasTemplateVariables(dashboardVariables)) {
|
||||
dispatch(
|
||||
notifyApp(createErrorNotification('This dashboard cannot be made public because it has template variables'))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
savePublicDashboardConfig(props.dashboard, publicDashboard, setPublicDashboardConfig).catch();
|
||||
};
|
||||
|
||||
const onAcknowledge = useCallback(
|
||||
(field: string, checked: boolean) => {
|
||||
setAcknowledgements({ ...acknowledgements, [field]: checked });
|
||||
},
|
||||
[acknowledgements]
|
||||
);
|
||||
|
||||
// check if all conditions have been acknowledged
|
||||
const acknowledged = acknowledgements.public && acknowledgements.datasources && acknowledgements.usage;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>Welcome to Grafana public dashboards alpha!</p>
|
||||
{dashboardHasTemplateVariables(dashboardVariables) ? (
|
||||
<Alert
|
||||
severity="warning"
|
||||
title="dashboard cannot be public"
|
||||
data-testid={selectors.TemplateVariablesWarningAlert}
|
||||
>
|
||||
This dashboard cannot be made public because it has template variables
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
To allow the current dashboard to be published publicly, toggle the switch. For now we do not support
|
||||
template variables or frontend datasources.
|
||||
</p>
|
||||
<p>
|
||||
We'd love your feedback. To share, please comment on this{' '}
|
||||
<a
|
||||
href="https://github.com/grafana/grafana/discussions/49253"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-link"
|
||||
>
|
||||
GitHub discussion
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<hr />
|
||||
<div className={styles.checkboxes}>
|
||||
<p>Before you click Save, please acknowledge the following information:</p>
|
||||
<FieldSet disabled={publicDashboardPersisted(publicDashboard) || !hasWritePermissions}>
|
||||
<VerticalGroup spacing="md">
|
||||
<HorizontalGroup spacing="none">
|
||||
<Checkbox
|
||||
label="Your entire dashboard will be public"
|
||||
value={acknowledgements.public}
|
||||
data-testid={selectors.WillBePublicCheckbox}
|
||||
onChange={(e) => onAcknowledge('public', e.currentTarget.checked)}
|
||||
/>
|
||||
<LinkButton
|
||||
variant="primary"
|
||||
href="https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/"
|
||||
target="_blank"
|
||||
fill="text"
|
||||
icon="info-circle"
|
||||
rel="noopener noreferrer"
|
||||
tooltip="Learn more about public dashboards"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="none">
|
||||
<Checkbox
|
||||
label="Publishing currently only works with a subset of datasources"
|
||||
value={acknowledgements.datasources}
|
||||
data-testid={selectors.LimitedDSCheckbox}
|
||||
onChange={(e) => onAcknowledge('datasources', e.currentTarget.checked)}
|
||||
/>
|
||||
<LinkButton
|
||||
variant="primary"
|
||||
href="https://grafana.com/docs/grafana/latest/datasources/"
|
||||
target="_blank"
|
||||
fill="text"
|
||||
icon="info-circle"
|
||||
rel="noopener noreferrer"
|
||||
tooltip="Learn more about public datasources"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="none">
|
||||
<Checkbox
|
||||
label="Making your dashboard public will cause queries to run each time the dashboard is viewed which may increase costs"
|
||||
value={acknowledgements.usage}
|
||||
data-testid={selectors.CostIncreaseCheckbox}
|
||||
onChange={(e) => onAcknowledge('usage', e.currentTarget.checked)}
|
||||
/>
|
||||
<LinkButton
|
||||
variant="primary"
|
||||
href="https://grafana.com/docs/grafana/latest/enterprise/query-caching/"
|
||||
target="_blank"
|
||||
fill="text"
|
||||
icon="info-circle"
|
||||
rel="noopener noreferrer"
|
||||
tooltip="Learn more about query caching"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</FieldSet>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<h4 className="share-modal-info-text">Public dashboard configuration</h4>
|
||||
<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">
|
||||
Time Range
|
||||
</Label>
|
||||
<TimeRangeInput value={timeRange} disabled onChange={() => {}} />
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="xs" justify="space-between">
|
||||
<Label description="Configures whether current dashboard can be available publicly">Enabled</Label>
|
||||
<Switch
|
||||
disabled={dashboardHasTemplateVariables(dashboardVariables)}
|
||||
data-testid={selectors.EnableSwitch}
|
||||
value={publicDashboard?.isEnabled}
|
||||
onChange={() => {
|
||||
reportInteraction('grafana_dashboards_public_enable_clicked', {
|
||||
action: publicDashboard?.isEnabled ? 'disable' : 'enable',
|
||||
});
|
||||
|
||||
setPublicDashboardConfig({
|
||||
...publicDashboard,
|
||||
isEnabled: !publicDashboard.isEnabled,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
{publicDashboardPersisted(publicDashboard) && publicDashboard.isEnabled && (
|
||||
<Field label="Link URL" className={styles.publicUrl}>
|
||||
<Input
|
||||
value={generatePublicDashboardUrl(publicDashboard)}
|
||||
readOnly
|
||||
data-testid={selectors.CopyUrlInput}
|
||||
addonAfter={
|
||||
<ClipboardButton
|
||||
data-testid={selectors.CopyUrlButton}
|
||||
variant="primary"
|
||||
icon="copy"
|
||||
getText={() => generatePublicDashboardUrl(publicDashboard)}
|
||||
>
|
||||
Copy
|
||||
</ClipboardButton>
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</FieldSet>
|
||||
{hasWritePermissions ? (
|
||||
props.dashboard.hasUnsavedChanges() && (
|
||||
<Alert
|
||||
title="Please save your dashboard changes before updating the public configuration"
|
||||
severity="warning"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Alert title="You don't have permissions to create or update a public dashboard" severity="warning" />
|
||||
)}
|
||||
<Button
|
||||
disabled={!hasWritePermissions || !acknowledged || props.dashboard.hasUnsavedChanges()}
|
||||
onClick={onSavePublicConfig}
|
||||
data-testid={selectors.SaveConfigButton}
|
||||
>
|
||||
Save sharing configuration
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
`,
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||
import { Checkbox, FieldSet, HorizontalGroup, LinkButton, VerticalGroup } from '@grafana/ui/src';
|
||||
|
||||
import { Acknowledgements } from './SharePublicDashboardUtils';
|
||||
|
||||
export const AcknowledgeCheckboxes = ({
|
||||
disabled,
|
||||
acknowledgements,
|
||||
onAcknowledge,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
acknowledgements: Acknowledgements;
|
||||
onAcknowledge: (key: string, val: boolean) => void;
|
||||
}) => {
|
||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>Before you click Save, please acknowledge the following information:</p>
|
||||
<FieldSet disabled={disabled}>
|
||||
<VerticalGroup spacing="md">
|
||||
<HorizontalGroup spacing="none">
|
||||
<Checkbox
|
||||
label="Your entire dashboard will be public"
|
||||
value={acknowledgements.public}
|
||||
data-testid={selectors.WillBePublicCheckbox}
|
||||
onChange={(e) => onAcknowledge('public', e.currentTarget.checked)}
|
||||
/>
|
||||
<LinkButton
|
||||
variant="primary"
|
||||
href="https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/"
|
||||
target="_blank"
|
||||
fill="text"
|
||||
icon="info-circle"
|
||||
rel="noopener noreferrer"
|
||||
tooltip="Learn more about public dashboards"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="none">
|
||||
<Checkbox
|
||||
label="Publishing currently only works with a subset of datasources"
|
||||
value={acknowledgements.datasources}
|
||||
data-testid={selectors.LimitedDSCheckbox}
|
||||
onChange={(e) => onAcknowledge('datasources', e.currentTarget.checked)}
|
||||
/>
|
||||
<LinkButton
|
||||
variant="primary"
|
||||
href="https://grafana.com/docs/grafana/latest/datasources/"
|
||||
target="_blank"
|
||||
fill="text"
|
||||
icon="info-circle"
|
||||
rel="noopener noreferrer"
|
||||
tooltip="Learn more about public datasources"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="none">
|
||||
<Checkbox
|
||||
label="Making your dashboard public will cause queries to run each time the dashboard is viewed which may increase costs"
|
||||
value={acknowledgements.usage}
|
||||
data-testid={selectors.CostIncreaseCheckbox}
|
||||
onChange={(e) => onAcknowledge('usage', e.currentTarget.checked)}
|
||||
/>
|
||||
<LinkButton
|
||||
variant="primary"
|
||||
href="https://grafana.com/docs/grafana/latest/enterprise/query-caching/"
|
||||
target="_blank"
|
||||
fill="text"
|
||||
icon="info-circle"
|
||||
rel="noopener noreferrer"
|
||||
tooltip="Learn more about query caching"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</FieldSet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||
import { reportInteraction } from '@grafana/runtime/src';
|
||||
import { FieldSet, Label, Switch, TimeRangeInput, useStyles2, VerticalGroup } from '@grafana/ui/src';
|
||||
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { useIsDesktop } from 'app/features/dashboard/utils/screen';
|
||||
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
|
||||
|
||||
export const Configuration = ({
|
||||
disabled,
|
||||
isPubDashEnabled,
|
||||
hasTemplateVariables,
|
||||
onToggleEnabled,
|
||||
dashboard,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
isPubDashEnabled?: boolean;
|
||||
onToggleEnabled: () => void;
|
||||
hasTemplateVariables: boolean;
|
||||
dashboard: DashboardModel;
|
||||
}) => {
|
||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||
const styles = useStyles2(getStyles);
|
||||
const isDesktop = useIsDesktop();
|
||||
|
||||
const timeRange = getTimeRange(dashboard.getDefaultTime(), dashboard);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="share-modal-info-text">Public dashboard configuration</h4>
|
||||
<FieldSet disabled={disabled} className={styles.dashboardConfig}>
|
||||
<VerticalGroup spacing="md">
|
||||
<Layout orientation={isDesktop ? 0 : 1} spacing="xs" justify="space-between">
|
||||
<Label description="The public dashboard uses the default time settings of the dashboard">Time range</Label>
|
||||
<TimeRangeInput value={timeRange} disabled onChange={() => {}} />
|
||||
</Layout>
|
||||
<Layout orientation={isDesktop ? 0 : 1} spacing="xs" justify="space-between">
|
||||
<Label description="Configures whether current dashboard can be available publicly">Enabled</Label>
|
||||
<Switch
|
||||
disabled={hasTemplateVariables}
|
||||
data-testid={selectors.EnableSwitch}
|
||||
value={isPubDashEnabled}
|
||||
onChange={() => {
|
||||
reportInteraction('grafana_dashboards_public_enable_clicked', {
|
||||
action: isPubDashEnabled ? 'disable' : 'enable',
|
||||
});
|
||||
|
||||
onToggleEnabled();
|
||||
}}
|
||||
/>
|
||||
</Layout>
|
||||
</VerticalGroup>
|
||||
</FieldSet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
dashboardConfig: css`
|
||||
margin: ${theme.spacing(0, 0, 3, 0)};
|
||||
`,
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Description = () => (
|
||||
<>
|
||||
<p>
|
||||
To allow the current dashboard to be published publicly, toggle the switch. For now we do not support template
|
||||
variables or frontend datasources.
|
||||
</p>
|
||||
<p>
|
||||
We'd love your feedback. To share, please comment on this{' '}
|
||||
<a
|
||||
href="https://github.com/grafana/grafana/discussions/49253"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-link"
|
||||
>
|
||||
GitHub discussion
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,283 @@
|
||||
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import 'whatwg-fetch';
|
||||
import { BootData } from '@grafana/data/src';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||
import { setEchoSrv } from '@grafana/runtime/src';
|
||||
import config from 'app/core/config';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { Echo } from 'app/core/services/echo/Echo';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { ShareModal } from '../ShareModal';
|
||||
|
||||
const server = setupServer(
|
||||
rest.get('/api/dashboards/uid/:uId/public-config', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
isEnabled: false,
|
||||
uid: undefined,
|
||||
dashboardUid: undefined,
|
||||
accessToken: 'an-access-token',
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
|
||||
const renderSharePublicDashboard = async (props: React.ComponentProps<typeof ShareModal>, isEnabled = true) => {
|
||||
const store = configureStore();
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ShareModal {...props} />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
await waitFor(() => screen.getByText('Link'));
|
||||
isEnabled && fireEvent.click(screen.getByText('Public dashboard'));
|
||||
};
|
||||
|
||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||
|
||||
let originalBootData: BootData;
|
||||
let mockDashboard: DashboardModel;
|
||||
let mockPanel: PanelModel;
|
||||
|
||||
beforeAll(() => {
|
||||
setEchoSrv(new Echo());
|
||||
originalBootData = config.bootData;
|
||||
config.appUrl = 'http://dashboards.grafana.com/';
|
||||
config.bootData = {
|
||||
user: {
|
||||
orgId: 1,
|
||||
},
|
||||
navTree: [
|
||||
{
|
||||
text: 'Section name',
|
||||
id: 'section',
|
||||
url: 'section',
|
||||
children: [
|
||||
{ text: 'Child1', id: 'child1', url: 'section/child1' },
|
||||
{ text: 'Child2', id: 'child2', url: 'section/child2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
server.listen({ onUnhandledRequest: 'bypass' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
config.featureToggles.publicDashboards = true;
|
||||
mockDashboard = new DashboardModel({
|
||||
uid: 'mockDashboardUid',
|
||||
});
|
||||
|
||||
mockPanel = new PanelModel({
|
||||
id: 'mockPanelId',
|
||||
});
|
||||
|
||||
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true);
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
||||
jest.spyOn(contextSrv, 'hasRole').mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.bootData = originalBootData;
|
||||
server.close();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('SharePublic', () => {
|
||||
it('does not render share panel when public dashboards feature is disabled', async () => {
|
||||
config.featureToggles.publicDashboards = false;
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }, false);
|
||||
|
||||
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
||||
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard');
|
||||
});
|
||||
|
||||
it('renders share panel when public dashboards feature is enabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
|
||||
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
||||
expect(screen.getByRole('tablist')).toHaveTextContent('Public dashboard');
|
||||
|
||||
fireEvent.click(screen.getByText('Public dashboard'));
|
||||
|
||||
await screen.findByText('Welcome to Grafana public dashboards alpha!');
|
||||
});
|
||||
|
||||
it('renders default relative time in input', async () => {
|
||||
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
|
||||
|
||||
//@ts-ignore
|
||||
mockDashboard.originalTime = { from: 'now-6h', to: 'now' };
|
||||
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
|
||||
await screen.findByText('Welcome to Grafana public dashboards alpha!');
|
||||
expect(screen.getByText('Last 6 hours')).toBeInTheDocument();
|
||||
});
|
||||
it('renders default absolute time in input 2', async () => {
|
||||
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' };
|
||||
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
|
||||
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();
|
||||
});
|
||||
it('when modal is opened, then loader spinner appears and inputs are disabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
|
||||
expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.EnableSwitch)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
||||
});
|
||||
it('when fetch errors happen, then all inputs remain disabled', async () => {
|
||||
server.use(
|
||||
rest.get('/api/dashboards/uid/:uId/public-config', (req, res, ctx) => {
|
||||
return res(ctx.status(500));
|
||||
})
|
||||
);
|
||||
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'), { timeout: 7000 });
|
||||
|
||||
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.EnableSwitch)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
||||
});
|
||||
// test checking if current version of dashboard in state is persisted to db
|
||||
});
|
||||
|
||||
describe('SharePublic - New config setup', () => {
|
||||
it('when modal is opened, then save button is disabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
||||
});
|
||||
it('when fetch is done, then loader spinner is gone, inputs are enabled and save button is disabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeEnabled();
|
||||
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeEnabled();
|
||||
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeEnabled();
|
||||
expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled();
|
||||
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
||||
});
|
||||
it('when checkboxes are filled, then save button remains disabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
|
||||
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
|
||||
fireEvent.click(screen.getByTestId(selectors.CostIncreaseCheckbox));
|
||||
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
||||
});
|
||||
it('when checkboxes and switch are filled, then save button is enabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
|
||||
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
|
||||
fireEvent.click(screen.getByTestId(selectors.CostIncreaseCheckbox));
|
||||
fireEvent.click(screen.getByTestId(selectors.EnableSwitch));
|
||||
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SharePublic - Already persisted', () => {
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get('/api/dashboards/uid/:uId/public-config', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
isEnabled: true,
|
||||
uid: 'a-uid',
|
||||
dashboardUid: req.params.uId,
|
||||
accessToken: 'an-access-token',
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('when modal is opened, then save button is enabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
|
||||
});
|
||||
it('when fetch is done, then loader spinner is gone, inputs are disabled and save button is enabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
|
||||
|
||||
expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled();
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
|
||||
});
|
||||
it('when pubdash is enabled, then link url is available', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
expect(screen.getByTestId(selectors.CopyUrlInput)).toBeInTheDocument();
|
||||
});
|
||||
it('when pubdash is disabled in the db, then link url is not available', async () => {
|
||||
server.use(
|
||||
rest.get('/api/dashboards/uid/:uId/public-config', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
isEnabled: false,
|
||||
uid: 'a-uid',
|
||||
dashboardUid: req.params.uId,
|
||||
accessToken: 'an-access-token',
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument();
|
||||
});
|
||||
it('when pubdash is disabled by the user, then link url is not available', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
fireEvent.click(screen.getByTestId(selectors.EnableSwitch));
|
||||
expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||
import { reportInteraction } from '@grafana/runtime/src';
|
||||
import { Alert, Button, ClipboardButton, Field, HorizontalGroup, Input, useStyles2, Spinner } from '@grafana/ui/src';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { useGetConfigQuery, useSaveConfigMutation } from 'app/features/dashboard/api/publicDashboardApi';
|
||||
import { AcknowledgeCheckboxes } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes';
|
||||
import { Configuration } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/Configuration';
|
||||
import { Description } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/Description';
|
||||
import {
|
||||
Acknowledgements,
|
||||
dashboardHasTemplateVariables,
|
||||
generatePublicDashboardUrl,
|
||||
publicDashboardPersisted,
|
||||
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
||||
import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types';
|
||||
import { isOrgAdmin } from 'app/features/plugins/admin/permissions';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
interface Props extends ShareModalTabProps {}
|
||||
|
||||
export const SharePublicDashboard = (props: Props) => {
|
||||
const dashboardVariables = props.dashboard.getVariables();
|
||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const {
|
||||
isLoading: isFetchingLoading,
|
||||
data: publicDashboard,
|
||||
isError: isFetchingError,
|
||||
} = useGetConfigQuery(props.dashboard.uid);
|
||||
|
||||
const [saveConfig, { isLoading: isSaveLoading }] = useSaveConfigMutation();
|
||||
|
||||
const [acknowledgements, setAcknowledgements] = useState<Acknowledgements>({
|
||||
public: false,
|
||||
datasources: false,
|
||||
usage: false,
|
||||
});
|
||||
const [enabledSwitch, setEnabledSwitch] = useState({
|
||||
isEnabled: false,
|
||||
wasTouched: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reportInteraction('grafana_dashboards_public_share_viewed');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (publicDashboardPersisted(publicDashboard)) {
|
||||
setAcknowledgements({
|
||||
public: true,
|
||||
datasources: true,
|
||||
usage: true,
|
||||
});
|
||||
}
|
||||
|
||||
setEnabledSwitch((prevState) => ({ ...prevState, isEnabled: !!publicDashboard?.isEnabled }));
|
||||
}, [publicDashboard]);
|
||||
|
||||
const isLoading = isFetchingLoading || isSaveLoading;
|
||||
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
|
||||
const acknowledged = acknowledgements.public && acknowledgements.datasources && acknowledgements.usage;
|
||||
const isSaveEnabled = useMemo(
|
||||
() =>
|
||||
!hasWritePermissions ||
|
||||
!acknowledged ||
|
||||
props.dashboard.hasUnsavedChanges() ||
|
||||
isLoading ||
|
||||
isFetchingError ||
|
||||
(!publicDashboardPersisted(publicDashboard) && !enabledSwitch.wasTouched),
|
||||
[hasWritePermissions, acknowledged, props.dashboard, isLoading, isFetchingError, enabledSwitch, publicDashboard]
|
||||
);
|
||||
|
||||
const onSavePublicConfig = () => {
|
||||
reportInteraction('grafana_dashboards_public_create_clicked');
|
||||
|
||||
if (dashboardHasTemplateVariables(dashboardVariables)) {
|
||||
dispatch(
|
||||
notifyApp(createErrorNotification('This dashboard cannot be made public because it has template variables'))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
saveConfig({
|
||||
dashboard: props.dashboard,
|
||||
payload: { ...publicDashboard!, isEnabled: enabledSwitch.isEnabled },
|
||||
});
|
||||
};
|
||||
|
||||
const onAcknowledge = (field: string, checked: boolean) => {
|
||||
setAcknowledgements((prevState) => ({ ...prevState, [field]: checked }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<HorizontalGroup>
|
||||
<p
|
||||
className={css`
|
||||
margin: 0;
|
||||
`}
|
||||
>
|
||||
Welcome to Grafana public dashboards alpha!
|
||||
</p>
|
||||
{isFetchingLoading && <Spinner />}
|
||||
</HorizontalGroup>
|
||||
<div className={styles.content}>
|
||||
{dashboardHasTemplateVariables(dashboardVariables) ? (
|
||||
<Alert
|
||||
severity="warning"
|
||||
title="dashboard cannot be public"
|
||||
data-testid={selectors.TemplateVariablesWarningAlert}
|
||||
>
|
||||
This dashboard cannot be made public because it has template variables
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Description />
|
||||
<hr />
|
||||
<div className={styles.checkboxes}>
|
||||
<AcknowledgeCheckboxes
|
||||
disabled={
|
||||
publicDashboardPersisted(publicDashboard) || !hasWritePermissions || isLoading || isFetchingError
|
||||
}
|
||||
acknowledgements={acknowledgements}
|
||||
onAcknowledge={onAcknowledge}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<Configuration
|
||||
dashboard={props.dashboard}
|
||||
disabled={!hasWritePermissions || isLoading || isFetchingError}
|
||||
isPubDashEnabled={enabledSwitch.isEnabled}
|
||||
onToggleEnabled={() =>
|
||||
setEnabledSwitch((prevState) => ({ isEnabled: !prevState.isEnabled, wasTouched: true }))
|
||||
}
|
||||
hasTemplateVariables={dashboardHasTemplateVariables(dashboardVariables)}
|
||||
/>
|
||||
{publicDashboardPersisted(publicDashboard) && enabledSwitch.isEnabled && (
|
||||
<Field label="Link URL" className={styles.publicUrl}>
|
||||
<Input
|
||||
disabled={isLoading}
|
||||
value={generatePublicDashboardUrl(publicDashboard!)}
|
||||
readOnly
|
||||
data-testid={selectors.CopyUrlInput}
|
||||
addonAfter={
|
||||
<ClipboardButton
|
||||
data-testid={selectors.CopyUrlButton}
|
||||
variant="primary"
|
||||
icon="copy"
|
||||
getText={() => generatePublicDashboardUrl(publicDashboard!)}
|
||||
>
|
||||
Copy
|
||||
</ClipboardButton>
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{hasWritePermissions ? (
|
||||
props.dashboard.hasUnsavedChanges() && (
|
||||
<Alert
|
||||
title="Please save your dashboard changes before updating the public configuration"
|
||||
severity="warning"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Alert title="You don't have permissions to create or update a public dashboard" severity="warning" />
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
<Button disabled={isSaveEnabled} onClick={onSavePublicConfig} data-testid={selectors.SaveConfigButton}>
|
||||
Save sharing configuration
|
||||
</Button>
|
||||
{isSaveLoading && <Spinner />}
|
||||
</HorizontalGroup>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
content: css`
|
||||
margin: ${theme.spacing(1, 0, 0, 0)};
|
||||
`,
|
||||
checkboxes: css`
|
||||
margin: ${theme.spacing(2, 0)};
|
||||
`,
|
||||
timeRange: css`
|
||||
padding: ${theme.spacing(1, 1)};
|
||||
margin: ${theme.spacing(0, 0, 2, 0)};
|
||||
`,
|
||||
publicUrl: css`
|
||||
width: 100%;
|
||||
margin-bottom: ${theme.spacing(0, 0, 3, 0)};
|
||||
`,
|
||||
});
|
||||
@@ -1,14 +1,11 @@
|
||||
import { updateConfig } from 'app/core/config';
|
||||
import { VariableModel } from 'app/features/variables/types';
|
||||
|
||||
import { updateConfig } from '../../../../core/config';
|
||||
|
||||
import {
|
||||
PublicDashboard,
|
||||
dashboardHasTemplateVariables,
|
||||
generatePublicDashboardUrl,
|
||||
publicDashboardPersisted,
|
||||
getPublicDashboardConfigUrl,
|
||||
savePublicDashboardConfigUrl,
|
||||
} from './SharePublicDashboardUtils';
|
||||
|
||||
describe('dashboardHasTemplateVariables', () => {
|
||||
@@ -48,15 +45,3 @@ describe('publicDashboardPersisted', () => {
|
||||
expect(publicDashboardPersisted(pubdash)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPublicDashboardConfigUrl', () => {
|
||||
it('builds the correct url', () => {
|
||||
expect(getPublicDashboardConfigUrl('abc1234')).toEqual('/api/dashboards/uid/abc1234/public-config');
|
||||
});
|
||||
});
|
||||
|
||||
describe('savePublicDashboardConfigUrl', () => {
|
||||
it('builds the correct url', () => {
|
||||
expect(savePublicDashboardConfigUrl('abc1234')).toEqual('/api/dashboards/uid/abc1234/public-config');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { VariableModel } from 'app/features/variables/types';
|
||||
import { DashboardDataDTO, DashboardMeta } from 'app/types/dashboard';
|
||||
|
||||
export interface PublicDashboard {
|
||||
accessToken?: string;
|
||||
isEnabled: boolean;
|
||||
uid: string;
|
||||
dashboardUid: string;
|
||||
timeSettings?: object;
|
||||
}
|
||||
|
||||
export interface DashboardResponse {
|
||||
dashboard: DashboardDataDTO;
|
||||
meta: DashboardMeta;
|
||||
}
|
||||
|
||||
export interface Acknowledgements {
|
||||
public: boolean;
|
||||
datasources: boolean;
|
||||
usage: boolean;
|
||||
}
|
||||
|
||||
// Instance methods
|
||||
export const dashboardHasTemplateVariables = (variables: VariableModel[]): boolean => {
|
||||
return variables.length > 0;
|
||||
};
|
||||
|
||||
export const publicDashboardPersisted = (publicDashboard?: PublicDashboard): boolean => {
|
||||
return publicDashboard?.uid !== '' && publicDashboard?.uid !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the public dashboard url. Uses the appUrl from the Grafana boot config, so urls will also be correct
|
||||
* when Grafana is hosted on a subpath.
|
||||
*
|
||||
* All app urls from the Grafana boot config end with a slash.
|
||||
*
|
||||
* @param publicDashboard
|
||||
*/
|
||||
export const generatePublicDashboardUrl = (publicDashboard: PublicDashboard): string => {
|
||||
return `${getConfig().appUrl}public-dashboards/${publicDashboard.accessToken}`;
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
import { VariableModel } from 'app/features/variables/types';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { DashboardDataDTO, DashboardMeta } from 'app/types/dashboard';
|
||||
|
||||
import { DashboardModel } from '../../state';
|
||||
|
||||
export interface PublicDashboard {
|
||||
accessToken?: string;
|
||||
isEnabled: boolean;
|
||||
uid: string;
|
||||
dashboardUid: string;
|
||||
timeSettings?: object;
|
||||
}
|
||||
|
||||
export interface DashboardResponse {
|
||||
dashboard: DashboardDataDTO;
|
||||
meta: DashboardMeta;
|
||||
}
|
||||
|
||||
export const getPublicDashboardConfig = async (
|
||||
dashboardUid: string,
|
||||
setPublicDashboard: React.Dispatch<React.SetStateAction<PublicDashboard>>
|
||||
) => {
|
||||
const pdResp: PublicDashboard = await getBackendSrv().get(getPublicDashboardConfigUrl(dashboardUid));
|
||||
setPublicDashboard(pdResp);
|
||||
};
|
||||
|
||||
export const savePublicDashboardConfig = async (
|
||||
dashboard: DashboardModel,
|
||||
publicDashboardConfig: PublicDashboard,
|
||||
setPublicDashboard: React.Dispatch<React.SetStateAction<PublicDashboard>>
|
||||
) => {
|
||||
const pdResp: PublicDashboard = await getBackendSrv().post(
|
||||
savePublicDashboardConfigUrl(dashboard.uid),
|
||||
publicDashboardConfig
|
||||
);
|
||||
|
||||
// Never allow a user to send the orgId
|
||||
// @ts-ignore
|
||||
delete pdResp.orgId;
|
||||
|
||||
dispatch(notifyApp(createSuccessNotification('Dashboard sharing configuration saved')));
|
||||
setPublicDashboard(pdResp);
|
||||
|
||||
// Update runtime emta flag
|
||||
dashboard.updateMeta({
|
||||
publicDashboardUid: pdResp.uid,
|
||||
publicDashboardEnabled: publicDashboardConfig.isEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPublicDashboardConfigUrl = (dashboardUid: string) => {
|
||||
return `/api/dashboards/uid/${dashboardUid}/public-config`;
|
||||
};
|
||||
|
||||
export const savePublicDashboardConfigUrl = (dashboardUid: string) => {
|
||||
return `/api/dashboards/uid/${dashboardUid}/public-config`;
|
||||
};
|
||||
|
||||
// Instance methods
|
||||
export const dashboardHasTemplateVariables = (variables: VariableModel[]): boolean => {
|
||||
return variables.length > 0;
|
||||
};
|
||||
|
||||
export const publicDashboardPersisted = (publicDashboard: PublicDashboard): boolean => {
|
||||
return publicDashboard.uid !== '' && publicDashboard.uid !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the public dashboard url. Uses the appUrl from the Grafana boot config, so urls will also be correct
|
||||
* when Grafana is hosted on a subpath.
|
||||
*
|
||||
* All app urls from the Grafana boot config end with a slash.
|
||||
*
|
||||
* @param publicDashboard
|
||||
*/
|
||||
export const generatePublicDashboardUrl = (publicDashboard: PublicDashboard): string => {
|
||||
return `${getConfig().appUrl}public-dashboards/${publicDashboard.accessToken}`;
|
||||
};
|
||||
Reference in New Issue
Block a user