mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PublicDashboards: Configuration modal redesign (#63211)
Configuration modal redesign --------- Co-authored-by: kay delaney <kay@grafana.com>
This commit is contained in:
@@ -18,32 +18,42 @@ e2e.scenario({
|
|||||||
// Select public dashboards tab
|
// Select public dashboards tab
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
|
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
|
||||||
|
|
||||||
// Saving button should be disabled
|
// Create button should be disabled
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.disabled');
|
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('be.disabled');
|
||||||
|
|
||||||
|
// Create flow shouldn't show these elements
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('not.exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().should('not.exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch().should('not.exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch().should('not.exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('not.exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.DeleteButton().should('not.exist');
|
||||||
|
|
||||||
// Acknowledge checkboxes
|
// Acknowledge checkboxes
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('be.enabled').click({ force: true });
|
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('be.enabled').click({ force: true });
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('be.enabled').click({ force: true });
|
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('be.enabled').click({ force: true });
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('be.enabled').click({ force: true });
|
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('be.enabled').click({ force: true });
|
||||||
|
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.disabled');
|
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('be.enabled');
|
||||||
|
|
||||||
// Switch on enabling toggle
|
// Create public dashboard
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.EnableSwitch().should('be.enabled').click({ force: true });
|
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.enabled');
|
|
||||||
|
|
||||||
// Save public dashboard
|
|
||||||
e2e().intercept('POST', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('save');
|
e2e().intercept('POST', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('save');
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().click();
|
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().click();
|
||||||
e2e().wait('@save');
|
e2e().wait('@save');
|
||||||
|
|
||||||
// Checkboxes should be disabled after saving public dashboard
|
// These elements shouldn't be rendered after creating public dashboard
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('be.disabled');
|
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('not.exist');
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('be.disabled');
|
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('not.exist');
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('be.disabled');
|
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('not.exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('not.exist');
|
||||||
|
|
||||||
// Save public dashboard button should still be enabled
|
// These elements should be rendered
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.enabled');
|
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().should('exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch().should('exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch().should('exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.DeleteButton().should('exist');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,7 +80,12 @@ e2e.scenario({
|
|||||||
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
|
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
|
||||||
e2e().wait('@query-public-dashboard');
|
e2e().wait('@query-public-dashboard');
|
||||||
|
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.enabled');
|
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().should('exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.EnableAnnotationsSwitch().should('exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.EnableTimeRangeSwitch().should('exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('exist');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.DeleteButton().should('exist');
|
||||||
|
|
||||||
// Make a request to public dashboards api endpoint without authentication
|
// Make a request to public dashboards api endpoint without authentication
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput()
|
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput()
|
||||||
@@ -106,29 +121,20 @@ e2e.scenario({
|
|||||||
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
|
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
|
||||||
e2e().wait('@query-public-dashboard');
|
e2e().wait('@query-public-dashboard');
|
||||||
|
|
||||||
// All checkboxes should be disabled
|
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('be.disabled');
|
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('be.disabled');
|
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('be.disabled');
|
|
||||||
|
|
||||||
// Saving button should be enabled
|
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.enabled');
|
|
||||||
|
|
||||||
// save url before disabling public dashboard
|
// save url before disabling public dashboard
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput()
|
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput()
|
||||||
.invoke('val')
|
.invoke('val')
|
||||||
.then((text) => e2e().wrap(text).as('url'));
|
.then((text) => e2e().wrap(text).as('url'));
|
||||||
|
|
||||||
// Switch off enabling toggle
|
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.EnableSwitch().should('be.enabled').click({ force: true });
|
|
||||||
|
|
||||||
// Save public dashboard
|
// Save public dashboard
|
||||||
e2e().intercept('PUT', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards/*').as('update');
|
e2e().intercept('PUT', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards/*').as('update');
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().click();
|
// Switch off enabling toggle
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('be.enabled').click({ force: true });
|
||||||
e2e().wait('@update');
|
e2e().wait('@update');
|
||||||
|
|
||||||
// Url should be hidden
|
// Url should be hidden
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('not.exist');
|
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput().should('be.disabled');
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlButton().should('be.disabled');
|
||||||
|
|
||||||
// Make a request to public dashboards api endpoint without authentication
|
// Make a request to public dashboards api endpoint without authentication
|
||||||
e2e()
|
e2e()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { e2e } from '@grafana/e2e';
|
import { e2e } from '@grafana/e2e';
|
||||||
|
|
||||||
e2e.scenario({
|
e2e.scenario({
|
||||||
describeName: 'Create a public dashboard with template variables is disabled',
|
describeName: 'Create a public dashboard with template variables shows a template variable warning',
|
||||||
itName: 'Create a public dashboard with template variables is disabled',
|
itName: 'Create a public dashboard with template variables shows a template variable warning',
|
||||||
addScenarioDataSource: false,
|
addScenarioDataSource: false,
|
||||||
addScenarioDashBoard: false,
|
addScenarioDashBoard: false,
|
||||||
skipScenario: false,
|
skipScenario: false,
|
||||||
@@ -20,10 +20,11 @@ e2e.scenario({
|
|||||||
e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible');
|
e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible');
|
||||||
|
|
||||||
// Configuration elements for public dashboards should not exist
|
// Configuration elements for public dashboards should not exist
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('not.exist');
|
e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('exist');
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('not.exist');
|
e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('exist');
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('not.exist');
|
e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('exist');
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.EnableSwitch().should('not.exist');
|
e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('exist');
|
||||||
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('not.exist');
|
|
||||||
|
e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('not.exist');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -190,14 +190,15 @@ export const Pages = {
|
|||||||
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',
|
||||||
EnableSwitch: 'data-testid public dashboard on off switch',
|
PauseSwitch: 'data-testid public dashboard pause switch',
|
||||||
EnableAnnotationsSwitch: 'data-testid public dashboard on off switch for annotations',
|
EnableAnnotationsSwitch: 'data-testid public dashboard on off switch for annotations',
|
||||||
SaveConfigButton: 'data-testid public dashboard save config button',
|
CreateButton: 'data-testid public dashboard create button',
|
||||||
DeleteButton: 'data-testid public dashboard delete button',
|
DeleteButton: 'data-testid public dashboard delete button',
|
||||||
CopyUrlInput: 'data-testid public dashboard copy url input',
|
CopyUrlInput: 'data-testid public dashboard copy url input',
|
||||||
CopyUrlButton: 'data-testid public dashboard copy url button',
|
CopyUrlButton: 'data-testid public dashboard copy url button',
|
||||||
TemplateVariablesWarningAlert: 'data-testid public dashboard disabled template variables alert',
|
TemplateVariablesWarningAlert: 'data-testid public dashboard disabled template variables alert',
|
||||||
UnsupportedDatasourcesWarningAlert: 'data-testid public dashboard unsupported datasources',
|
UnsupportedDataSourcesWarningAlert: 'data-testid public dashboard unsupported data sources alert',
|
||||||
|
NoUpsertPermissionsWarningAlert: 'data-testid public dashboard no upsert permissions alert',
|
||||||
EnableTimeRangeSwitch: 'data-testid public dashboard on off switch for time range',
|
EnableTimeRangeSwitch: 'data-testid public dashboard on off switch for time range',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={cx(styles.wrapper, className)}>
|
<label className={cx(styles.wrapper, className)}>
|
||||||
<div>
|
<div className={styles.checkboxWrapper}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
@@ -43,10 +43,8 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
|||||||
/>
|
/>
|
||||||
<span className={styles.checkmark} />
|
<span className={styles.checkmark} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{label && <span className={styles.label}>{label}</span>}
|
||||||
{label && <span className={styles.label}>{label}</span>}
|
{description && <span className={styles.description}>{description}</span>}
|
||||||
{description && <span className={styles.description}>{description}</span>}
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -59,12 +57,12 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
display: inline-flex;
|
display: grid;
|
||||||
gap: ${theme.spacing(labelPadding)};
|
align-items: center;
|
||||||
align-items: baseline;
|
column-gap: ${theme.spacing(labelPadding)};
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
vertical-align: middle;
|
|
||||||
font-size: 0;
|
|
||||||
`,
|
`,
|
||||||
input: css`
|
input: css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -125,6 +123,12 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
checkboxWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
grid-column-start: 1;
|
||||||
|
grid-row-start: 1;
|
||||||
|
`,
|
||||||
checkmark: css`
|
checkmark: css`
|
||||||
position: relative; /* Checkbox should be layered on top of the invisible input so it recieves :hover */
|
position: relative; /* Checkbox should be layered on top of the invisible input so it recieves :hover */
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -143,10 +147,11 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
label: cx(
|
label: cx(
|
||||||
labelStyles.label,
|
labelStyles.label,
|
||||||
css`
|
css`
|
||||||
|
grid-column-start: 2;
|
||||||
|
grid-row-start: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
top: -3px;
|
|
||||||
max-width: fit-content;
|
max-width: fit-content;
|
||||||
line-height: ${theme.typography.bodySmall.lineHeight};
|
line-height: ${theme.typography.bodySmall.lineHeight};
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@@ -155,6 +160,8 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
description: cx(
|
description: cx(
|
||||||
labelStyles.description,
|
labelStyles.description,
|
||||||
css`
|
css`
|
||||||
|
grid-column-start: 2;
|
||||||
|
grid-row-start: 2;
|
||||||
line-height: ${theme.typography.bodySmall.lineHeight};
|
line-height: ${theme.typography.bodySmall.lineHeight};
|
||||||
margin-top: 0; /* The margin effectively comes from the top: -2px on the label above it */
|
margin-top: 0; /* The margin effectively comes from the top: -2px on the label above it */
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Tooltip } from '../../../Tooltip/Tooltip';
|
|||||||
export interface Props {
|
export interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
labelClass?: string;
|
labelClass?: string;
|
||||||
switchClass?: string;
|
switchClass?: string;
|
||||||
@@ -37,6 +38,7 @@ export class Switch extends PureComponent<Props, State> {
|
|||||||
switchClass = '',
|
switchClass = '',
|
||||||
label,
|
label,
|
||||||
checked,
|
checked,
|
||||||
|
disabled,
|
||||||
transparent,
|
transparent,
|
||||||
className,
|
className,
|
||||||
tooltip,
|
tooltip,
|
||||||
@@ -63,7 +65,13 @@ export class Switch extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={switchClassName}>
|
<div className={switchClassName}>
|
||||||
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
|
<input
|
||||||
|
disabled={disabled}
|
||||||
|
id={labelId}
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={this.internalOnChange}
|
||||||
|
/>
|
||||||
<span className="gf-form-switch__slider" />
|
<span className="gf-form-switch__slider" />
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { lastValueFrom } from 'rxjs';
|
|||||||
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime/src';
|
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime/src';
|
||||||
import { notifyApp } from 'app/core/actions';
|
import { notifyApp } from 'app/core/actions';
|
||||||
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
|
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
|
||||||
import { PublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
import {
|
||||||
|
PublicDashboard,
|
||||||
|
PublicDashboardSettings,
|
||||||
|
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
import { ListPublicDashboardResponse } from 'app/features/manage-dashboards/types';
|
import { ListPublicDashboardResponse } from 'app/features/manage-dashboards/types';
|
||||||
|
|
||||||
@@ -55,7 +58,10 @@ export const publicDashboardApi = createApi({
|
|||||||
},
|
},
|
||||||
providesTags: (result, error, dashboardUid) => [{ type: 'PublicDashboard', id: dashboardUid }],
|
providesTags: (result, error, dashboardUid) => [{ type: 'PublicDashboard', id: dashboardUid }],
|
||||||
}),
|
}),
|
||||||
createPublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
|
createPublicDashboard: builder.mutation<
|
||||||
|
PublicDashboard,
|
||||||
|
{ dashboard: DashboardModel; payload: Partial<PublicDashboardSettings> }
|
||||||
|
>({
|
||||||
query: (params) => ({
|
query: (params) => ({
|
||||||
url: `/uid/${params.dashboard.uid}/public-dashboards`,
|
url: `/uid/${params.dashboard.uid}/public-dashboards`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -63,7 +69,7 @@ export const publicDashboardApi = createApi({
|
|||||||
}),
|
}),
|
||||||
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
|
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
|
||||||
const { data } = await queryFulfilled;
|
const { data } = await queryFulfilled;
|
||||||
dispatch(notifyApp(createSuccessNotification('Public dashboard created!')));
|
dispatch(notifyApp(createSuccessNotification('Dashboard is public!')));
|
||||||
|
|
||||||
// Update runtime meta flag
|
// Update runtime meta flag
|
||||||
dashboard.updateMeta({
|
dashboard.updateMeta({
|
||||||
@@ -71,7 +77,7 @@ export const publicDashboardApi = createApi({
|
|||||||
publicDashboardEnabled: data.isEnabled,
|
publicDashboardEnabled: data.isEnabled,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
invalidatesTags: (result, error, { payload }) => [{ type: 'PublicDashboard', id: payload.dashboardUid }],
|
invalidatesTags: (result, error, { dashboard }) => [{ type: 'PublicDashboard', id: dashboard.uid }],
|
||||||
}),
|
}),
|
||||||
updatePublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
|
updatePublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
|
||||||
query: (params) => ({
|
query: (params) => ({
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { UseFormRegister } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
|
||||||
import { Checkbox, FieldSet, HorizontalGroup, LinkButton, VerticalGroup } from '@grafana/ui/src';
|
|
||||||
|
|
||||||
import { SharePublicDashboardAcknowledgmentInputs, SharePublicDashboardInputs } from './SharePublicDashboard';
|
|
||||||
|
|
||||||
type Acknowledge = {
|
|
||||||
type: keyof SharePublicDashboardAcknowledgmentInputs;
|
|
||||||
description: string;
|
|
||||||
testId: string;
|
|
||||||
info: {
|
|
||||||
href: string;
|
|
||||||
tooltip: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
|
||||||
|
|
||||||
const ACKNOWLEDGES: Acknowledge[] = [
|
|
||||||
{
|
|
||||||
type: 'publicAcknowledgment',
|
|
||||||
description: 'Your entire dashboard will be public',
|
|
||||||
testId: selectors.WillBePublicCheckbox,
|
|
||||||
info: {
|
|
||||||
href: 'https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/',
|
|
||||||
tooltip: 'Learn more about public dashboards',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'dataSourcesAcknowledgment',
|
|
||||||
description: 'Publishing currently only works with a subset of datasources',
|
|
||||||
testId: selectors.LimitedDSCheckbox,
|
|
||||||
info: {
|
|
||||||
href: 'https://grafana.com/docs/grafana/latest/datasources/',
|
|
||||||
tooltip: 'Learn more about public datasources',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'usageAcknowledgment',
|
|
||||||
description: 'Making a dashboard public causes queries to run each time it is viewed, which may increase costs',
|
|
||||||
testId: selectors.CostIncreaseCheckbox,
|
|
||||||
info: {
|
|
||||||
href: 'https://grafana.com/docs/grafana/latest/enterprise/query-caching/',
|
|
||||||
tooltip: 'Learn more about query caching',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const AcknowledgeCheckboxes = ({
|
|
||||||
disabled,
|
|
||||||
register,
|
|
||||||
}: {
|
|
||||||
disabled: boolean;
|
|
||||||
register: UseFormRegister<SharePublicDashboardInputs>;
|
|
||||||
}) => (
|
|
||||||
<>
|
|
||||||
<p>Before you click Save, please acknowledge the following information:</p>
|
|
||||||
<FieldSet disabled={disabled}>
|
|
||||||
<VerticalGroup spacing="md">
|
|
||||||
{ACKNOWLEDGES.map((acknowledge) => (
|
|
||||||
<HorizontalGroup key={acknowledge.type} spacing="none" align="center">
|
|
||||||
<Checkbox
|
|
||||||
{...register(acknowledge.type)}
|
|
||||||
label={acknowledge.description}
|
|
||||||
data-testid={acknowledge.testId}
|
|
||||||
/>
|
|
||||||
<LinkButton
|
|
||||||
variant="primary"
|
|
||||||
href={acknowledge.info.href}
|
|
||||||
target="_blank"
|
|
||||||
fill="text"
|
|
||||||
icon="info-circle"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
tooltip={acknowledge.info.tooltip}
|
|
||||||
/>
|
|
||||||
</HorizontalGroup>
|
|
||||||
))}
|
|
||||||
</VerticalGroup>
|
|
||||||
</FieldSet>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||||
|
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||||
|
import { reportInteraction } from '@grafana/runtime/src';
|
||||||
|
import {
|
||||||
|
ClipboardButton,
|
||||||
|
Field,
|
||||||
|
HorizontalGroup,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
ModalsContext,
|
||||||
|
Spinner,
|
||||||
|
Switch,
|
||||||
|
useStyles2,
|
||||||
|
} from '@grafana/ui/src';
|
||||||
|
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
||||||
|
|
||||||
|
import { contextSrv } from '../../../../../../core/services/context_srv';
|
||||||
|
import { AccessControlAction, useSelector } from '../../../../../../types';
|
||||||
|
import { DeletePublicDashboardButton } from '../../../../../manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton';
|
||||||
|
import { isOrgAdmin } from '../../../../../plugins/admin/permissions';
|
||||||
|
import { useGetPublicDashboardQuery, useUpdatePublicDashboardMutation } from '../../../../api/publicDashboardApi';
|
||||||
|
import { useIsDesktop } from '../../../../utils/screen';
|
||||||
|
import { ShareModal } from '../../ShareModal';
|
||||||
|
import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAlert';
|
||||||
|
import { SaveDashboardChangesAlert } from '../ModalAlerts/SaveDashboardChangesAlert';
|
||||||
|
import { UnsupportedDataSourcesAlert } from '../ModalAlerts/UnsupportedDataSourcesAlert';
|
||||||
|
import { UnsupportedTemplateVariablesAlert } from '../ModalAlerts/UnsupportedTemplateVariablesAlert';
|
||||||
|
import {
|
||||||
|
dashboardHasTemplateVariables,
|
||||||
|
generatePublicDashboardUrl,
|
||||||
|
getUnsupportedDashboardDatasources,
|
||||||
|
} from '../SharePublicDashboardUtils';
|
||||||
|
|
||||||
|
import { Configuration } from './Configuration';
|
||||||
|
|
||||||
|
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||||
|
|
||||||
|
export interface ConfigPublicDashoardForm {
|
||||||
|
isAnnotationsEnabled: boolean;
|
||||||
|
isTimeSelectionEnabled: boolean;
|
||||||
|
isPaused: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfigPublicDashboard = () => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const { showModal, hideModal } = useContext(ModalsContext);
|
||||||
|
const isDesktop = useIsDesktop();
|
||||||
|
|
||||||
|
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
|
||||||
|
const dashboardState = useSelector((store) => store.dashboard);
|
||||||
|
const dashboard = dashboardState.getModel()!;
|
||||||
|
const dashboardVariables = dashboard.getVariables();
|
||||||
|
const unsupportedDataSources = getUnsupportedDashboardDatasources(dashboard.panels);
|
||||||
|
|
||||||
|
const { data: publicDashboard, isFetching: isGetLoading } = useGetPublicDashboardQuery(dashboard.uid);
|
||||||
|
const [update, { isLoading: isUpdateLoading }] = useUpdatePublicDashboardMutation();
|
||||||
|
const disableInputs = !hasWritePermissions || isUpdateLoading || isGetLoading;
|
||||||
|
|
||||||
|
const { handleSubmit, setValue, register } = useForm<ConfigPublicDashoardForm>({
|
||||||
|
defaultValues: {
|
||||||
|
isAnnotationsEnabled: publicDashboard?.annotationsEnabled,
|
||||||
|
isTimeSelectionEnabled: publicDashboard?.timeSelectionEnabled,
|
||||||
|
isPaused: !publicDashboard?.isEnabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUpdate = async (values: ConfigPublicDashoardForm) => {
|
||||||
|
const { isAnnotationsEnabled, isTimeSelectionEnabled, isPaused } = values;
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
dashboard,
|
||||||
|
payload: {
|
||||||
|
...publicDashboard!,
|
||||||
|
annotationsEnabled: isAnnotationsEnabled,
|
||||||
|
timeSelectionEnabled: isTimeSelectionEnabled,
|
||||||
|
isEnabled: !isPaused,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
update(req);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChange = async (name: keyof ConfigPublicDashoardForm, value: boolean) => {
|
||||||
|
setValue(name, value);
|
||||||
|
await handleSubmit((data) => onUpdate(data))();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDismissDelete = () => {
|
||||||
|
showModal(ShareModal, {
|
||||||
|
dashboard,
|
||||||
|
onDismiss: hideModal,
|
||||||
|
activeTab: 'share',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{hasWritePermissions && dashboard.hasUnsavedChanges() && <SaveDashboardChangesAlert />}
|
||||||
|
{!hasWritePermissions && <NoUpsertPermissionsAlert mode="edit" />}
|
||||||
|
{dashboardHasTemplateVariables(dashboardVariables) && <UnsupportedTemplateVariablesAlert />}
|
||||||
|
{!!unsupportedDataSources.length && (
|
||||||
|
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDataSources.join(', ')} />
|
||||||
|
)}
|
||||||
|
<div className={styles.titleContainer}>
|
||||||
|
<HorizontalGroup spacing="sm" align="center">
|
||||||
|
<h4 className={styles.title}>Settings</h4>
|
||||||
|
{(isUpdateLoading || isGetLoading) && <Spinner size={14} />}
|
||||||
|
</HorizontalGroup>
|
||||||
|
</div>
|
||||||
|
<Configuration disabled={disableInputs} onChange={onChange} register={register} />
|
||||||
|
<hr />
|
||||||
|
<Field label="Dashboard URL" className={styles.publicUrl}>
|
||||||
|
<Input
|
||||||
|
value={generatePublicDashboardUrl(publicDashboard!)}
|
||||||
|
readOnly
|
||||||
|
disabled={!publicDashboard?.isEnabled}
|
||||||
|
data-testid={selectors.CopyUrlInput}
|
||||||
|
addonAfter={
|
||||||
|
<ClipboardButton
|
||||||
|
data-testid={selectors.CopyUrlButton}
|
||||||
|
variant="primary"
|
||||||
|
disabled={!publicDashboard?.isEnabled}
|
||||||
|
getText={() => generatePublicDashboardUrl(publicDashboard!)}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</ClipboardButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Layout
|
||||||
|
orientation={isDesktop ? 0 : 1}
|
||||||
|
justify={isDesktop ? 'flex-end' : 'flex-start'}
|
||||||
|
align={isDesktop ? 'center' : 'normal'}
|
||||||
|
>
|
||||||
|
<HorizontalGroup spacing="sm">
|
||||||
|
<Switch
|
||||||
|
{...register('isPaused')}
|
||||||
|
disabled={disableInputs}
|
||||||
|
onChange={(e) => {
|
||||||
|
reportInteraction('grafana_dashboards_public_enable_clicked', {
|
||||||
|
action: e.currentTarget.checked ? 'disable' : 'enable',
|
||||||
|
});
|
||||||
|
onChange('isPaused', e.currentTarget.checked);
|
||||||
|
}}
|
||||||
|
data-testid={selectors.PauseSwitch}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
className={css`
|
||||||
|
margin-bottom: 0;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Pause sharing dashboard
|
||||||
|
</Label>
|
||||||
|
</HorizontalGroup>
|
||||||
|
<HorizontalGroup justify="flex-end">
|
||||||
|
<DeletePublicDashboardButton
|
||||||
|
className={cx(styles.deleteButton, { [styles.deleteButtonMobile]: !isDesktop })}
|
||||||
|
type="button"
|
||||||
|
disabled={disableInputs}
|
||||||
|
data-testid={selectors.DeleteButton}
|
||||||
|
onDismiss={onDismissDelete}
|
||||||
|
variant="destructive"
|
||||||
|
fill="outline"
|
||||||
|
dashboard={dashboard}
|
||||||
|
publicDashboard={{
|
||||||
|
uid: publicDashboard!.uid,
|
||||||
|
dashboardUid: dashboard.uid,
|
||||||
|
title: dashboard.title,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Revoke public URL
|
||||||
|
</DeletePublicDashboardButton>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
titleContainer: css`
|
||||||
|
margin-bottom: ${theme.spacing(2)};
|
||||||
|
`,
|
||||||
|
title: css`
|
||||||
|
margin: 0;
|
||||||
|
`,
|
||||||
|
publicUrl: css`
|
||||||
|
width: 100%;
|
||||||
|
padding-top: ${theme.spacing(1)};
|
||||||
|
margin-bottom: ${theme.spacing(3)};
|
||||||
|
`,
|
||||||
|
deleteButton: css`
|
||||||
|
margin-left: ${theme.spacing(3)}; ;
|
||||||
|
`,
|
||||||
|
deleteButtonMobile: css`
|
||||||
|
margin-top: ${theme.spacing(2)}; ;
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ConfigPublicDashboard;
|
||||||
@@ -7,69 +7,60 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
|||||||
import { reportInteraction } from '@grafana/runtime/src';
|
import { reportInteraction } from '@grafana/runtime/src';
|
||||||
import { FieldSet, Label, Switch, TimeRangeInput, useStyles2, VerticalGroup } from '@grafana/ui/src';
|
import { FieldSet, Label, Switch, TimeRangeInput, useStyles2, VerticalGroup } from '@grafana/ui/src';
|
||||||
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
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';
|
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
|
||||||
|
|
||||||
import { SharePublicDashboardInputs } from './SharePublicDashboard';
|
import { useSelector } from '../../../../../../types';
|
||||||
|
|
||||||
|
import { ConfigPublicDashoardForm } from './ConfigPublicDashboard';
|
||||||
|
|
||||||
|
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||||
|
|
||||||
export const Configuration = ({
|
export const Configuration = ({
|
||||||
disabled,
|
disabled,
|
||||||
dashboard,
|
onChange,
|
||||||
register,
|
register,
|
||||||
}: {
|
}: {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
dashboard: DashboardModel;
|
onChange: (name: keyof ConfigPublicDashoardForm, value: boolean) => void;
|
||||||
register: UseFormRegister<SharePublicDashboardInputs>;
|
register: UseFormRegister<ConfigPublicDashoardForm>;
|
||||||
}) => {
|
}) => {
|
||||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const isDesktop = useIsDesktop();
|
|
||||||
|
const dashboardState = useSelector((store) => store.dashboard);
|
||||||
|
const dashboard = dashboardState.getModel()!;
|
||||||
|
|
||||||
const timeRange = getTimeRange(dashboard.getDefaultTime(), dashboard);
|
const timeRange = getTimeRange(dashboard.getDefaultTime(), dashboard);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4 className={styles.title}>Public dashboard configuration</h4>
|
|
||||||
<FieldSet disabled={disabled} className={styles.dashboardConfig}>
|
<FieldSet disabled={disabled} className={styles.dashboardConfig}>
|
||||||
<VerticalGroup spacing="md">
|
<VerticalGroup spacing="md">
|
||||||
<Layout orientation={isDesktop ? 0 : 1} spacing="xs" justify="space-between">
|
<Layout orientation={1} 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 range settings of the dashboard">
|
||||||
Default time range
|
Default time range
|
||||||
</Label>
|
</Label>
|
||||||
<TimeRangeInput value={timeRange} disabled onChange={() => {}} />
|
<TimeRangeInput value={timeRange} disabled onChange={() => {}} />
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout orientation={isDesktop ? 0 : 1} spacing="xs" justify="space-between">
|
<Layout orientation={0} spacing="sm">
|
||||||
|
<Switch
|
||||||
|
{...register('isTimeSelectionEnabled')}
|
||||||
|
data-testid={selectors.EnableTimeRangeSwitch}
|
||||||
|
onChange={(e) => onChange('isTimeSelectionEnabled', e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
<Label description="Allow viewers to change time range">Time range picker enabled</Label>
|
<Label description="Allow viewers to change time range">Time range picker enabled</Label>
|
||||||
<Switch {...register('isTimeRangeEnabled')} data-testid={selectors.EnableTimeRangeSwitch} />
|
|
||||||
</Layout>
|
</Layout>
|
||||||
<Layout orientation={isDesktop ? 0 : 1} spacing="xs" justify="space-between">
|
<Layout orientation={0} spacing="sm">
|
||||||
<Label description="Show annotations on public dashboard">Show annotations</Label>
|
|
||||||
<Switch
|
<Switch
|
||||||
{...register('isAnnotationsEnabled')}
|
{...register('isAnnotationsEnabled')}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const { onChange } = register('isAnnotationsEnabled');
|
|
||||||
reportInteraction('grafana_dashboards_annotations_clicked', {
|
reportInteraction('grafana_dashboards_annotations_clicked', {
|
||||||
action: e.currentTarget.checked ? 'enable' : 'disable',
|
action: e.currentTarget.checked ? 'enable' : 'disable',
|
||||||
});
|
});
|
||||||
onChange(e);
|
onChange('isAnnotationsEnabled', e.currentTarget.checked);
|
||||||
}}
|
}}
|
||||||
data-testid={selectors.EnableAnnotationsSwitch}
|
data-testid={selectors.EnableAnnotationsSwitch}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
<Label description="Show annotations on public dashboard">Show annotations</Label>
|
||||||
<Layout orientation={isDesktop ? 0 : 1} spacing="xs" justify="space-between">
|
|
||||||
<Label description="Configures whether current dashboard can be available publicly">Enabled</Label>
|
|
||||||
<Switch
|
|
||||||
{...register('enabledSwitch')}
|
|
||||||
onChange={(e) => {
|
|
||||||
const { onChange } = register('enabledSwitch');
|
|
||||||
reportInteraction('grafana_dashboards_public_enable_clicked', {
|
|
||||||
action: e.currentTarget.checked ? 'enable' : 'disable',
|
|
||||||
});
|
|
||||||
onChange(e);
|
|
||||||
}}
|
|
||||||
data-testid={selectors.EnableSwitch}
|
|
||||||
/>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</VerticalGroup>
|
</VerticalGroup>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
@@ -78,16 +69,7 @@ export const Configuration = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
title: css`
|
|
||||||
margin-bottom: ${theme.spacing(2)};
|
|
||||||
`,
|
|
||||||
dashboardConfig: css`
|
dashboardConfig: css`
|
||||||
margin: ${theme.spacing(0, 0, 3, 0)};
|
margin: ${theme.spacing(0, 0, 3, 0)};
|
||||||
`,
|
`,
|
||||||
timeRange: css`
|
|
||||||
margin-bottom: ${theme.spacing(0)};
|
|
||||||
`,
|
|
||||||
timeRangeDisabledText: css`
|
|
||||||
font-size: ${theme.typography.bodySmall.fontSize};
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
import { UseFormRegister } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||||
|
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||||
|
import { Checkbox, FieldSet, HorizontalGroup, LinkButton, useStyles2, VerticalGroup } from '@grafana/ui/src';
|
||||||
|
|
||||||
|
import { SharePublicDashboardAcknowledgmentInputs } from './CreatePublicDashboard';
|
||||||
|
|
||||||
|
type Acknowledge = {
|
||||||
|
type: keyof SharePublicDashboardAcknowledgmentInputs;
|
||||||
|
description: string;
|
||||||
|
testId: string;
|
||||||
|
info: {
|
||||||
|
href: string;
|
||||||
|
tooltip: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||||
|
|
||||||
|
const ACKNOWLEDGES: Acknowledge[] = [
|
||||||
|
{
|
||||||
|
type: 'publicAcknowledgment',
|
||||||
|
description: 'Your entire dashboard will be public*',
|
||||||
|
testId: selectors.WillBePublicCheckbox,
|
||||||
|
info: {
|
||||||
|
href: 'https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/',
|
||||||
|
tooltip: 'Learn more about public dashboards',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'dataSourcesAcknowledgment',
|
||||||
|
description: 'Publishing currently only works with a subset of data sources*',
|
||||||
|
testId: selectors.LimitedDSCheckbox,
|
||||||
|
info: {
|
||||||
|
href: 'https://grafana.com/docs/grafana/latest/datasources/',
|
||||||
|
tooltip: 'Learn more about public datasources',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'usageAcknowledgment',
|
||||||
|
description: 'Making a dashboard public will cause queries to run each time is viewed, which may increase costs*',
|
||||||
|
testId: selectors.CostIncreaseCheckbox,
|
||||||
|
info: {
|
||||||
|
href: 'https://grafana.com/docs/grafana/latest/enterprise/query-caching/',
|
||||||
|
tooltip: 'Learn more about query caching',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AcknowledgeCheckboxes = ({
|
||||||
|
disabled,
|
||||||
|
register,
|
||||||
|
}: {
|
||||||
|
disabled: boolean;
|
||||||
|
register: UseFormRegister<SharePublicDashboardAcknowledgmentInputs>;
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className={styles.title}>Before you make the dashboard public, acknowledge the following:</p>
|
||||||
|
<FieldSet disabled={disabled}>
|
||||||
|
<VerticalGroup spacing="md">
|
||||||
|
{ACKNOWLEDGES.map((acknowledge) => (
|
||||||
|
<HorizontalGroup key={acknowledge.type} spacing="none" align="center">
|
||||||
|
<Checkbox
|
||||||
|
{...register(acknowledge.type, { required: true })}
|
||||||
|
label={acknowledge.description}
|
||||||
|
data-testid={acknowledge.testId}
|
||||||
|
/>
|
||||||
|
<LinkButton
|
||||||
|
variant="primary"
|
||||||
|
href={acknowledge.info.href}
|
||||||
|
target="_blank"
|
||||||
|
fill="text"
|
||||||
|
icon="info-circle"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
tooltip={acknowledge.info.tooltip}
|
||||||
|
/>
|
||||||
|
</HorizontalGroup>
|
||||||
|
))}
|
||||||
|
</VerticalGroup>
|
||||||
|
</FieldSet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
title: css`
|
||||||
|
font-weight: ${theme.typography.fontWeightBold};
|
||||||
|
`,
|
||||||
|
});
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
import { FormState, UseFormRegister } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||||
|
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||||
|
import { reportInteraction } from '@grafana/runtime/src';
|
||||||
|
import { Button, Form, Spinner, useStyles2 } from '@grafana/ui/src';
|
||||||
|
|
||||||
|
import { contextSrv } from '../../../../../../core/services/context_srv';
|
||||||
|
import { AccessControlAction, useSelector } from '../../../../../../types';
|
||||||
|
import { isOrgAdmin } from '../../../../../plugins/admin/permissions';
|
||||||
|
import { useCreatePublicDashboardMutation } from '../../../../api/publicDashboardApi';
|
||||||
|
import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAlert';
|
||||||
|
import { UnsupportedDataSourcesAlert } from '../ModalAlerts/UnsupportedDataSourcesAlert';
|
||||||
|
import { UnsupportedTemplateVariablesAlert } from '../ModalAlerts/UnsupportedTemplateVariablesAlert';
|
||||||
|
import { dashboardHasTemplateVariables, getUnsupportedDashboardDatasources } from '../SharePublicDashboardUtils';
|
||||||
|
|
||||||
|
import { AcknowledgeCheckboxes } from './AcknowledgeCheckboxes';
|
||||||
|
import { Description } from './Description';
|
||||||
|
|
||||||
|
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||||
|
|
||||||
|
export type SharePublicDashboardAcknowledgmentInputs = {
|
||||||
|
publicAcknowledgment: boolean;
|
||||||
|
dataSourcesAcknowledgment: boolean;
|
||||||
|
usageAcknowledgment: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreatePublicDashboard = ({ isError }: { isError: boolean }) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
|
||||||
|
const dashboardState = useSelector((store) => store.dashboard);
|
||||||
|
const dashboard = dashboardState.getModel()!;
|
||||||
|
const unsupportedDataSources = getUnsupportedDashboardDatasources(dashboard.panels);
|
||||||
|
|
||||||
|
const [createPublicDashboard, { isLoading: isSaveLoading }] = useCreatePublicDashboardMutation();
|
||||||
|
|
||||||
|
const disableInputs = !hasWritePermissions || isSaveLoading || isError;
|
||||||
|
|
||||||
|
const onCreate = async () => {
|
||||||
|
reportInteraction('grafana_dashboards_public_create_clicked');
|
||||||
|
createPublicDashboard({ dashboard, payload: { isEnabled: true } });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className={styles.title}>Welcome to public dashboards alpha!</p>
|
||||||
|
<Description />
|
||||||
|
{!hasWritePermissions && <NoUpsertPermissionsAlert mode="create" />}
|
||||||
|
{dashboardHasTemplateVariables(dashboard.getVariables()) && <UnsupportedTemplateVariablesAlert />}
|
||||||
|
{!!unsupportedDataSources.length && (
|
||||||
|
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDataSources.join(', ')} />
|
||||||
|
)}
|
||||||
|
<Form onSubmit={onCreate} validateOn="onChange" maxWidth="none">
|
||||||
|
{({
|
||||||
|
register,
|
||||||
|
formState: { isValid },
|
||||||
|
}: {
|
||||||
|
register: UseFormRegister<SharePublicDashboardAcknowledgmentInputs>;
|
||||||
|
formState: FormState<SharePublicDashboardAcknowledgmentInputs>;
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
<div className={styles.checkboxes}>
|
||||||
|
<AcknowledgeCheckboxes disabled={disableInputs} register={register} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<Button type="submit" disabled={disableInputs || !isValid} data-testid={selectors.CreateButton}>
|
||||||
|
Generate public URL {isSaveLoading && <Spinner className={styles.loadingSpinner} />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
title: css`
|
||||||
|
font-size: ${theme.typography.h4.fontSize};
|
||||||
|
margin: ${theme.spacing(0, 0, 2)};
|
||||||
|
`,
|
||||||
|
checkboxes: css`
|
||||||
|
margin: ${theme.spacing(0, 0, 4)};
|
||||||
|
`,
|
||||||
|
buttonContainer: css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
`,
|
||||||
|
loadingSpinner: css`
|
||||||
|
margin-left: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CreatePublicDashboard;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||||
|
import { useStyles2 } from '@grafana/ui/src';
|
||||||
|
|
||||||
|
export const Description = () => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p className={styles.description}>Currently, we don’t support template variables or frontend data sources</p>
|
||||||
|
<p className={styles.description}>
|
||||||
|
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={cx('text-link', styles.description)}
|
||||||
|
>
|
||||||
|
GitHub discussion
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
container: css`
|
||||||
|
margin-bottom: ${theme.spacing(3)};
|
||||||
|
`,
|
||||||
|
description: css`
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
margin-bottom: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
});
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||||
|
import { Alert } from '@grafana/ui/src';
|
||||||
|
|
||||||
|
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||||
|
|
||||||
|
export const NoUpsertPermissionsAlert = ({ mode }: { mode: 'create' | 'edit' }) => (
|
||||||
|
<Alert
|
||||||
|
severity="info"
|
||||||
|
title={`You don’t have permission to ${mode} a public dashboard`}
|
||||||
|
data-testid={selectors.NoUpsertPermissionsWarningAlert}
|
||||||
|
>
|
||||||
|
Contact your admin to get permission to {mode} create public dashboards
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Alert } from '@grafana/ui/src';
|
||||||
|
|
||||||
|
export const SaveDashboardChangesAlert = () => (
|
||||||
|
<Alert title="Please save your dashboard changes before updating the public configuration" severity="warning" />
|
||||||
|
);
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||||
|
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||||
|
import { Alert, useStyles2 } from '@grafana/ui/src';
|
||||||
|
|
||||||
|
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||||
|
|
||||||
|
export const UnsupportedDataSourcesAlert = ({ unsupportedDataSources }: { unsupportedDataSources: string }) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
severity="warning"
|
||||||
|
title="Unsupported data sources"
|
||||||
|
data-testid={selectors.UnsupportedDataSourcesWarningAlert}
|
||||||
|
>
|
||||||
|
<p className={styles.unsupportedDataSourceDescription}>
|
||||||
|
There are data sources in this dashboard that are unsupported for public dashboards. Panels that use these data
|
||||||
|
sources may not function properly: {unsupportedDataSources}.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/"
|
||||||
|
className={cx('text-link', styles.unsupportedDataSourceDescription)}
|
||||||
|
>
|
||||||
|
Read more about supported data sources
|
||||||
|
</a>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
unsupportedDataSourceDescription: css`
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
margin-bottom: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||||
|
import { Alert } from '@grafana/ui/src';
|
||||||
|
|
||||||
|
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||||
|
|
||||||
|
export const UnsupportedTemplateVariablesAlert = () => (
|
||||||
|
<Alert
|
||||||
|
severity="warning"
|
||||||
|
title="Template variables are not supported"
|
||||||
|
data-testid={selectors.TemplateVariablesWarningAlert}
|
||||||
|
>
|
||||||
|
This public dashboard may not work since it uses template variables
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
@@ -17,9 +17,10 @@ import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
|||||||
import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
|
import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
|
|
||||||
|
import { DashboardInitPhase } from '../../../../../types';
|
||||||
import { ShareModal } from '../ShareModal';
|
import { ShareModal } from '../ShareModal';
|
||||||
|
|
||||||
import { PublicDashboard } from './SharePublicDashboardUtils';
|
import * as sharePublicDashboardUtils from './SharePublicDashboardUtils';
|
||||||
|
|
||||||
const server = setupServer();
|
const server = setupServer();
|
||||||
|
|
||||||
@@ -28,17 +29,39 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
getBackendSrv: () => backendSrv,
|
getBackendSrv: () => backendSrv,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const renderSharePublicDashboard = async (props: React.ComponentProps<typeof ShareModal>, isEnabled = true) => {
|
const renderSharePublicDashboard = async (
|
||||||
const store = configureStore();
|
props?: Partial<React.ComponentProps<typeof ShareModal>>,
|
||||||
|
isEnabled = true
|
||||||
|
) => {
|
||||||
|
const store = configureStore({
|
||||||
|
dashboard: {
|
||||||
|
getModel: () => props?.dashboard || mockDashboard,
|
||||||
|
permissions: [],
|
||||||
|
initError: null,
|
||||||
|
initPhase: DashboardInitPhase.Completed,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const newProps = Object.assign(
|
||||||
|
{
|
||||||
|
panel: mockPanel,
|
||||||
|
dashboard: mockDashboard,
|
||||||
|
onDismiss: () => {},
|
||||||
|
},
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ShareModal {...props} />
|
<ShareModal {...newProps} />
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => screen.getByText('Link'));
|
await waitFor(() => screen.getByText('Link'));
|
||||||
isEnabled && fireEvent.click(screen.getByText('Public dashboard'));
|
if (isEnabled) {
|
||||||
|
fireEvent.click(screen.getByText('Public dashboard'));
|
||||||
|
await waitForElementToBeRemoved(screen.getByText('Loading configuration'));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||||
@@ -97,139 +120,54 @@ afterEach(() => {
|
|||||||
server.resetHandlers();
|
server.resetHandlers();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SharePublic', () => {
|
const pubdashResponse: sharePublicDashboardUtils.PublicDashboard = {
|
||||||
const pubdashResponse: PublicDashboard = {
|
isEnabled: true,
|
||||||
isEnabled: true,
|
annotationsEnabled: true,
|
||||||
annotationsEnabled: true,
|
timeSelectionEnabled: true,
|
||||||
timeSelectionEnabled: true,
|
uid: 'a-uid',
|
||||||
uid: 'a-uid',
|
dashboardUid: '',
|
||||||
dashboardUid: '',
|
accessToken: 'an-access-token',
|
||||||
accessToken: 'an-access-token',
|
};
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
const getExistentPublicDashboardResponse = () =>
|
||||||
server.use(
|
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
|
||||||
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
|
return res(
|
||||||
return res(
|
ctx.status(200),
|
||||||
ctx.status(200),
|
ctx.json({
|
||||||
ctx.json({
|
...pubdashResponse,
|
||||||
...pubdashResponse,
|
dashboardUid: req.params.dashboardUid,
|
||||||
dashboardUid: req.params.dashboardUid,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
const getNonExistentPublicDashboardResponse = () =>
|
||||||
it('does not render share panel when public dashboards feature is disabled', async () => {
|
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
|
||||||
config.featureToggles.publicDashboards = false;
|
return res(
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }, false);
|
ctx.status(404),
|
||||||
|
ctx.json({
|
||||||
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
message: 'Public dashboard not found',
|
||||||
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard');
|
messageId: 'publicdashboards.notFound',
|
||||||
|
statusCode: 404,
|
||||||
|
traceID: '',
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
it('renders share panel when public dashboards feature is enabled', async () => {
|
const getErrorPublicDashboardResponse = () =>
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
|
||||||
|
return res(ctx.status(500));
|
||||||
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!');
|
|
||||||
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
it('renders public dashboard modal without delete button because no public dashboard was already created', async () => {
|
|
||||||
|
const alertTests = () => {
|
||||||
|
it('when user has no write permissions, warning is shown', async () => {
|
||||||
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
|
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
|
||||||
|
|
||||||
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
await renderSharePublicDashboard();
|
||||||
expect(screen.getByRole('tablist')).toHaveTextContent('Public dashboard');
|
expect(screen.queryByTestId(selectors.NoUpsertPermissionsWarningAlert)).toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('Public dashboard'));
|
|
||||||
|
|
||||||
await screen.findByText('Welcome to Grafana public dashboards alpha!');
|
|
||||||
|
|
||||||
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
it('renders default relative time in input', async () => {
|
it('when dashboard has template variables, warning is shown', async () => {
|
||||||
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
|
jest.spyOn(sharePublicDashboardUtils, 'dashboardHasTemplateVariables').mockReturnValue(true);
|
||||||
|
|
||||||
//@ts-ignore
|
await renderSharePublicDashboard();
|
||||||
mockDashboard.originalTime = { from: 'now-6h', to: 'now' };
|
expect(screen.queryByTestId(selectors.TemplateVariablesWarningAlert)).toBeInTheDocument();
|
||||||
|
|
||||||
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 00:59:59')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('when modal is opened, then loader spinner appears and inputs are disabled', async () => {
|
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
|
||||||
|
|
||||||
screen.getAllByTestId('Spinner');
|
|
||||||
|
|
||||||
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
|
|
||||||
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
|
|
||||||
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
|
|
||||||
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeDisabled();
|
|
||||||
expect(screen.getByTestId(selectors.EnableSwitch)).toBeDisabled();
|
|
||||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
|
||||||
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('when fetch errors happen, then all inputs remain disabled', async () => {
|
|
||||||
server.use(
|
|
||||||
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
|
|
||||||
return res(ctx.status(500));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
|
||||||
|
|
||||||
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.EnableTimeRangeSwitch)).toBeDisabled();
|
|
||||||
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeDisabled();
|
|
||||||
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
|
||||||
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('SharePublic - New config setup', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
server.use(
|
|
||||||
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
|
|
||||||
return res(
|
|
||||||
ctx.status(404),
|
|
||||||
ctx.json({
|
|
||||||
message: 'Public dashboard not found',
|
|
||||||
messageId: 'publicdashboards.notFound',
|
|
||||||
statusCode: 404,
|
|
||||||
traceID: '',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
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 dashboard has unsupported datasources, warning is shown', async () => {
|
it('when dashboard has unsupported datasources, warning is shown', async () => {
|
||||||
const panelModel = {
|
const panelModel = {
|
||||||
@@ -244,108 +182,136 @@ describe('SharePublic - New config setup', () => {
|
|||||||
panels: [panelModel],
|
panels: [panelModel],
|
||||||
});
|
});
|
||||||
|
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard, onDismiss: () => {} });
|
await renderSharePublicDashboard({ dashboard });
|
||||||
|
expect(screen.queryByTestId(selectors.UnsupportedDataSourcesWarningAlert)).toBeInTheDocument();
|
||||||
expect(screen.queryByTestId(selectors.UnsupportedDatasourcesWarningAlert)).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
it('when fetch is done, then no loader spinner appears, inputs are enabled and save button is disabled', async () => {
|
};
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
describe('SharePublic', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
server.use(getExistentPublicDashboardResponse());
|
||||||
|
});
|
||||||
|
it('does not render share panel when public dashboards feature is disabled', async () => {
|
||||||
|
config.featureToggles.publicDashboards = false;
|
||||||
|
await renderSharePublicDashboard(undefined, false);
|
||||||
|
|
||||||
|
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
||||||
|
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard');
|
||||||
|
});
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
expect(screen.getByText('2022-08-30 00:00:00 to 2022-09-04 00:59:59')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('when modal is opened, then checkboxes are enabled but create button is disabled', async () => {
|
||||||
|
server.use(getNonExistentPublicDashboardResponse());
|
||||||
|
await renderSharePublicDashboard();
|
||||||
|
|
||||||
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeEnabled();
|
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeEnabled();
|
||||||
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeEnabled();
|
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeEnabled();
|
||||||
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeEnabled();
|
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeEnabled();
|
||||||
expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled();
|
expect(screen.getByTestId(selectors.CreateButton)).toBeDisabled();
|
||||||
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled();
|
|
||||||
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeEnabled();
|
|
||||||
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
|
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
|
||||||
|
|
||||||
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
|
||||||
});
|
});
|
||||||
it('when checkboxes are filled, then save button remains disabled', async () => {
|
it('when fetch errors happen, then all inputs remain disabled', async () => {
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
server.use(getErrorPublicDashboardResponse());
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
|
await renderSharePublicDashboard();
|
||||||
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
|
|
||||||
fireEvent.click(screen.getByTestId(selectors.CostIncreaseCheckbox));
|
|
||||||
|
|
||||||
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
|
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
|
||||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
|
||||||
});
|
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
|
||||||
it('when checkboxes and switch are filled, then save button is enabled', async () => {
|
expect(screen.getByTestId(selectors.CreateButton)).toBeDisabled();
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('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();
|
|
||||||
});
|
|
||||||
it('when hasPublicDashboard flag is false, then button text is Create', async () => {
|
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
|
||||||
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SharePublic - Already persisted', () => {
|
describe('SharePublic - New config setup', () => {
|
||||||
const pubdashResponse: PublicDashboard = {
|
|
||||||
isEnabled: true,
|
|
||||||
annotationsEnabled: true,
|
|
||||||
timeSelectionEnabled: true,
|
|
||||||
uid: 'a-uid',
|
|
||||||
dashboardUid: '',
|
|
||||||
accessToken: 'an-access-token',
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
server.use(
|
server.use(getNonExistentPublicDashboardResponse());
|
||||||
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
|
|
||||||
return res(
|
|
||||||
ctx.status(200),
|
|
||||||
ctx.json({
|
|
||||||
...pubdashResponse,
|
|
||||||
dashboardUid: req.params.dashboardUid,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when modal is opened, then save button and delete button are enabled', async () => {
|
it('renders when public dashboards feature is enabled', async () => {
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
await renderSharePublicDashboard();
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
|
||||||
|
|
||||||
expect(screen.getByTestId(selectors.DeleteButton)).toBeEnabled();
|
await screen.findByText('Welcome to public dashboards alpha!');
|
||||||
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
|
expect(screen.getByText('Generate public URL')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
|
|
||||||
});
|
expect(screen.queryByTestId(selectors.WillBePublicCheckbox)).toBeInTheDocument();
|
||||||
it('delete button is not rendered because lack of permissions', async () => {
|
expect(screen.queryByTestId(selectors.LimitedDSCheckbox)).toBeInTheDocument();
|
||||||
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
|
expect(screen.queryByTestId(selectors.CostIncreaseCheckbox)).toBeInTheDocument();
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
expect(screen.queryByTestId(selectors.CreateButton)).toBeInTheDocument();
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
|
||||||
|
|
||||||
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
|
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('when modal is opened, then annotations toggle is enabled and checked when its enabled in the db', async () => {
|
it('when modal is opened, then create button is disabled', async () => {
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
await renderSharePublicDashboard();
|
||||||
|
expect(screen.getByTestId(selectors.CreateButton)).toBeDisabled();
|
||||||
|
});
|
||||||
|
it('when checkboxes are filled, then create button is enabled', async () => {
|
||||||
|
await renderSharePublicDashboard();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
|
||||||
|
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
|
||||||
|
fireEvent.click(screen.getByTestId(selectors.CostIncreaseCheckbox));
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByTestId(selectors.CreateButton)).toBeEnabled());
|
||||||
|
});
|
||||||
|
alertTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SharePublic - Already persisted', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
server.use(getExistentPublicDashboardResponse());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when modal is opened, then delete button is enabled', async () => {
|
||||||
|
await renderSharePublicDashboard();
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
||||||
|
expect(screen.getByTestId(selectors.DeleteButton)).toBeEnabled();
|
||||||
|
});
|
||||||
|
it('when fetch is done, then inputs are checked and delete button is enabled', async () => {
|
||||||
|
await renderSharePublicDashboard();
|
||||||
|
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
||||||
|
|
||||||
|
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeEnabled();
|
||||||
|
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeChecked();
|
||||||
|
|
||||||
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled();
|
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled();
|
||||||
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeChecked();
|
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeChecked();
|
||||||
});
|
|
||||||
it('when modal is opened, then time range switch is enabled and checked when its checked in the db', async () => {
|
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
|
||||||
|
|
||||||
const enableTimeRangeSwitch = screen.getByTestId(selectors.EnableTimeRangeSwitch);
|
expect(screen.getByTestId(selectors.PauseSwitch)).toBeEnabled();
|
||||||
expect(enableTimeRangeSwitch).toBeEnabled();
|
expect(screen.getByTestId(selectors.PauseSwitch)).not.toBeChecked();
|
||||||
expect(enableTimeRangeSwitch).toBeChecked();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
expect(screen.getByTestId(selectors.DeleteButton)).toBeEnabled();
|
||||||
|
});
|
||||||
|
it('inputs and delete button are disabled because of lack of permissions', async () => {
|
||||||
|
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
|
||||||
|
await renderSharePublicDashboard();
|
||||||
|
|
||||||
|
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeDisabled();
|
||||||
|
expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeChecked();
|
||||||
|
|
||||||
|
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeDisabled();
|
||||||
|
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeChecked();
|
||||||
|
|
||||||
|
expect(screen.getByTestId(selectors.PauseSwitch)).toBeDisabled();
|
||||||
|
expect(screen.getByTestId(selectors.PauseSwitch)).not.toBeChecked();
|
||||||
|
|
||||||
|
expect(screen.queryByTestId(selectors.DeleteButton)).toBeDisabled();
|
||||||
|
});
|
||||||
it('when modal is opened, then time range switch is enabled and not checked when its not checked in the db', async () => {
|
it('when modal is opened, then time range switch is enabled and not checked when its not checked in the db', async () => {
|
||||||
server.use(
|
server.use(
|
||||||
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
|
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
|
||||||
@@ -359,33 +325,18 @@ describe('SharePublic - Already persisted', () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
await renderSharePublicDashboard();
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
||||||
|
|
||||||
const enableTimeRangeSwitch = screen.getByTestId(selectors.EnableTimeRangeSwitch);
|
const enableTimeRangeSwitch = screen.getByTestId(selectors.EnableTimeRangeSwitch);
|
||||||
expect(enableTimeRangeSwitch).toBeEnabled();
|
expect(enableTimeRangeSwitch).toBeEnabled();
|
||||||
expect(enableTimeRangeSwitch).not.toBeChecked();
|
expect(enableTimeRangeSwitch).not.toBeChecked();
|
||||||
});
|
});
|
||||||
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.getAllByTestId('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.EnableTimeRangeSwitch)).toBeEnabled();
|
|
||||||
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
|
|
||||||
expect(screen.getByTestId(selectors.DeleteButton)).toBeEnabled();
|
|
||||||
});
|
|
||||||
it('when pubdash is enabled, then link url is available', async () => {
|
it('when pubdash is enabled, then link url is available', async () => {
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
await renderSharePublicDashboard();
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
|
||||||
expect(screen.getByTestId(selectors.CopyUrlInput)).toBeInTheDocument();
|
expect(screen.getByTestId(selectors.CopyUrlInput)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('when pubdash is disabled in the db, then link url is not available and annotations toggle is disabled', async () => {
|
it('when pubdash is disabled in the db, then link url is not copyable and switch is checked', async () => {
|
||||||
server.use(
|
server.use(
|
||||||
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
|
rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => {
|
||||||
return res(
|
return res(
|
||||||
@@ -401,22 +352,13 @@ describe('SharePublic - Already persisted', () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
await renderSharePublicDashboard();
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
||||||
|
|
||||||
expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument();
|
expect(screen.queryByTestId(selectors.CopyUrlInput)).toBeInTheDocument();
|
||||||
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).not.toBeChecked();
|
expect(screen.queryByTestId(selectors.CopyUrlButton)).not.toBeChecked();
|
||||||
});
|
|
||||||
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.getAllByTestId('Spinner'));
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId(selectors.EnableSwitch));
|
expect(screen.getByTestId(selectors.PauseSwitch)).toBeChecked();
|
||||||
expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('when hasPublicDashboard flag is true, then button text is Save', async () => {
|
|
||||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
|
||||||
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
|
|
||||||
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
alertTests();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,307 +1,60 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useContext, useEffect, useMemo } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { Subscription } from 'rxjs';
|
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
|
||||||
import { reportInteraction } from '@grafana/runtime/src';
|
import { reportInteraction } from '@grafana/runtime/src';
|
||||||
import {
|
import { Spinner, useStyles2 } from '@grafana/ui/src';
|
||||||
Alert,
|
import { useGetPublicDashboardQuery } from 'app/features/dashboard/api/publicDashboardApi';
|
||||||
Button,
|
import { publicDashboardPersisted } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
||||||
ClipboardButton,
|
|
||||||
Field,
|
|
||||||
HorizontalGroup,
|
|
||||||
Input,
|
|
||||||
useStyles2,
|
|
||||||
Spinner,
|
|
||||||
ModalsContext,
|
|
||||||
useForceUpdate,
|
|
||||||
} from '@grafana/ui/src';
|
|
||||||
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
|
||||||
import {
|
|
||||||
useGetPublicDashboardQuery,
|
|
||||||
useCreatePublicDashboardMutation,
|
|
||||||
useUpdatePublicDashboardMutation,
|
|
||||||
} 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 {
|
|
||||||
dashboardHasTemplateVariables,
|
|
||||||
generatePublicDashboardUrl,
|
|
||||||
getUnsupportedDashboardDatasources,
|
|
||||||
publicDashboardPersisted,
|
|
||||||
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
|
||||||
import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types';
|
import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types';
|
||||||
import { useIsDesktop } from 'app/features/dashboard/utils/screen';
|
|
||||||
import { DeletePublicDashboardButton } from 'app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton';
|
|
||||||
import { isOrgAdmin } from 'app/features/plugins/admin/permissions';
|
|
||||||
import { AccessControlAction } from 'app/types';
|
|
||||||
|
|
||||||
import { DashboardMetaChangedEvent } from '../../../../../types/events';
|
import { HorizontalGroup } from '../../../../plugins/admin/components/HorizontalGroup';
|
||||||
import { ShareModal } from '../ShareModal';
|
|
||||||
|
|
||||||
|
import ConfigPublicDashboard from './ConfigPublicDashboard/ConfigPublicDashboard';
|
||||||
|
import CreatePublicDashboard from './CreatePublicDashboard/CreatePublicDashboard';
|
||||||
interface Props extends ShareModalTabProps {}
|
interface Props extends ShareModalTabProps {}
|
||||||
|
|
||||||
export type SharePublicDashboardAcknowledgmentInputs = {
|
const Loader = () => {
|
||||||
publicAcknowledgment: boolean;
|
const styles = useStyles2(getStyles);
|
||||||
dataSourcesAcknowledgment: boolean;
|
|
||||||
usageAcknowledgment: boolean;
|
return (
|
||||||
|
<HorizontalGroup className={styles.loadingContainer}>
|
||||||
|
<>
|
||||||
|
Loading configuration
|
||||||
|
<Spinner size={20} className={styles.spinner} />
|
||||||
|
</>
|
||||||
|
</HorizontalGroup>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SharePublicDashboardInputs = {
|
|
||||||
isAnnotationsEnabled: boolean;
|
|
||||||
enabledSwitch: boolean;
|
|
||||||
isTimeRangeEnabled: boolean;
|
|
||||||
} & SharePublicDashboardAcknowledgmentInputs;
|
|
||||||
|
|
||||||
export const SharePublicDashboard = (props: Props) => {
|
export const SharePublicDashboard = (props: Props) => {
|
||||||
const forceUpdate = useForceUpdate();
|
const { data: publicDashboard, isLoading, isError } = useGetPublicDashboardQuery(props.dashboard.uid);
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
const { showModal, hideModal } = useContext(ModalsContext);
|
|
||||||
const isDesktop = useIsDesktop();
|
|
||||||
|
|
||||||
const dashboardVariables = props.dashboard.getVariables();
|
|
||||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isLoading: isGetLoading,
|
|
||||||
data: publicDashboard,
|
|
||||||
isError: isGetError,
|
|
||||||
isFetching,
|
|
||||||
} = useGetPublicDashboardQuery(props.dashboard.uid);
|
|
||||||
|
|
||||||
const {
|
|
||||||
reset,
|
|
||||||
handleSubmit,
|
|
||||||
watch,
|
|
||||||
register,
|
|
||||||
formState: { dirtyFields },
|
|
||||||
} = useForm<SharePublicDashboardInputs>({
|
|
||||||
defaultValues: {
|
|
||||||
publicAcknowledgment: false,
|
|
||||||
dataSourcesAcknowledgment: false,
|
|
||||||
usageAcknowledgment: false,
|
|
||||||
isAnnotationsEnabled: false,
|
|
||||||
isTimeRangeEnabled: false,
|
|
||||||
enabledSwitch: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [createPublicDashboard, { isLoading: isSaveLoading }] = useCreatePublicDashboardMutation();
|
|
||||||
const [updatePublicDashboard, { isLoading: isUpdateLoading }] = useUpdatePublicDashboardMutation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const eventSubs = new Subscription();
|
|
||||||
eventSubs.add(props.dashboard.events.subscribe(DashboardMetaChangedEvent, forceUpdate));
|
|
||||||
reportInteraction('grafana_dashboards_public_share_viewed');
|
reportInteraction('grafana_dashboards_public_share_viewed');
|
||||||
|
}, []);
|
||||||
return () => eventSubs.unsubscribe();
|
|
||||||
}, [props.dashboard.events, forceUpdate]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const isPublicDashboardPersisted = publicDashboardPersisted(publicDashboard);
|
|
||||||
reset({
|
|
||||||
publicAcknowledgment: isPublicDashboardPersisted,
|
|
||||||
dataSourcesAcknowledgment: isPublicDashboardPersisted,
|
|
||||||
usageAcknowledgment: isPublicDashboardPersisted,
|
|
||||||
isAnnotationsEnabled: publicDashboard?.annotationsEnabled,
|
|
||||||
isTimeRangeEnabled: publicDashboard?.timeSelectionEnabled,
|
|
||||||
enabledSwitch: publicDashboard?.isEnabled,
|
|
||||||
});
|
|
||||||
}, [publicDashboard, reset]);
|
|
||||||
|
|
||||||
const isLoading = isGetLoading || isSaveLoading || isUpdateLoading;
|
|
||||||
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
|
|
||||||
const acknowledged =
|
|
||||||
watch('publicAcknowledgment') && watch('dataSourcesAcknowledgment') && watch('usageAcknowledgment');
|
|
||||||
|
|
||||||
const isSaveDisabled = useMemo(
|
|
||||||
() =>
|
|
||||||
!hasWritePermissions ||
|
|
||||||
!acknowledged ||
|
|
||||||
props.dashboard.hasUnsavedChanges() ||
|
|
||||||
isLoading ||
|
|
||||||
isFetching ||
|
|
||||||
isGetError ||
|
|
||||||
(!publicDashboardPersisted(publicDashboard) && !dirtyFields.enabledSwitch),
|
|
||||||
[
|
|
||||||
hasWritePermissions,
|
|
||||||
acknowledged,
|
|
||||||
props.dashboard,
|
|
||||||
isLoading,
|
|
||||||
isGetError,
|
|
||||||
publicDashboard,
|
|
||||||
isFetching,
|
|
||||||
dirtyFields.enabledSwitch,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isDeleteDisabled = isLoading || isFetching || isGetError;
|
|
||||||
|
|
||||||
const onSavePublicConfig = async (values: SharePublicDashboardInputs) => {
|
|
||||||
reportInteraction('grafana_dashboards_public_create_clicked');
|
|
||||||
|
|
||||||
const req = {
|
|
||||||
dashboard: props.dashboard,
|
|
||||||
payload: {
|
|
||||||
...publicDashboard!,
|
|
||||||
isEnabled: values.enabledSwitch,
|
|
||||||
annotationsEnabled: values.isAnnotationsEnabled,
|
|
||||||
timeSelectionEnabled: values.isTimeRangeEnabled,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// create or update based on whether we have existing uid
|
|
||||||
!!publicDashboard ? updatePublicDashboard(req) : createPublicDashboard(req);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDismissDelete = () => {
|
|
||||||
showModal(ShareModal, {
|
|
||||||
dashboard: props.dashboard,
|
|
||||||
onDismiss: hideModal,
|
|
||||||
activeTab: 'share',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HorizontalGroup>
|
{isLoading ? (
|
||||||
<p
|
<Loader />
|
||||||
className={css`
|
) : !publicDashboardPersisted(publicDashboard) ? (
|
||||||
margin: 0;
|
<CreatePublicDashboard isError={isError} />
|
||||||
`}
|
) : (
|
||||||
>
|
<ConfigPublicDashboard />
|
||||||
Welcome to Grafana public dashboards alpha!
|
)}
|
||||||
</p>
|
|
||||||
{(isGetLoading || isFetching) && <Spinner />}
|
|
||||||
</HorizontalGroup>
|
|
||||||
<div className={styles.content}>
|
|
||||||
{getUnsupportedDashboardDatasources(props.dashboard.panels).length > 0 ? (
|
|
||||||
<Alert
|
|
||||||
severity="warning"
|
|
||||||
title="Unsupported Datasources"
|
|
||||||
data-testid={selectors.UnsupportedDatasourcesWarningAlert}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{`There are datasources in this dashboard that are unsupported for public dashboards. Panels that use these datasources may not function properly: ${getUnsupportedDashboardDatasources(
|
|
||||||
props.dashboard.panels
|
|
||||||
).join(', ')}. See the `}
|
|
||||||
<a href="https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/" className="text-link">
|
|
||||||
docs
|
|
||||||
</a>{' '}
|
|
||||||
for supported datasources.
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
) : null}
|
|
||||||
{dashboardHasTemplateVariables(dashboardVariables) && !publicDashboardPersisted(publicDashboard) ? (
|
|
||||||
<Alert
|
|
||||||
severity="warning"
|
|
||||||
title="dashboard cannot be public"
|
|
||||||
data-testid={selectors.TemplateVariablesWarningAlert}
|
|
||||||
>
|
|
||||||
This dashboard cannot be made public because it has template variables
|
|
||||||
</Alert>
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleSubmit(onSavePublicConfig)}>
|
|
||||||
<Description />
|
|
||||||
<hr />
|
|
||||||
<div className={styles.checkboxes}>
|
|
||||||
<AcknowledgeCheckboxes
|
|
||||||
disabled={publicDashboardPersisted(publicDashboard) || !hasWritePermissions || isLoading || isGetError}
|
|
||||||
register={register}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<Configuration
|
|
||||||
register={register}
|
|
||||||
dashboard={props.dashboard}
|
|
||||||
disabled={!hasWritePermissions || isLoading || isGetError}
|
|
||||||
/>
|
|
||||||
{publicDashboardPersisted(publicDashboard) && watch('enabledSwitch') && (
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
dashboardHasTemplateVariables(dashboardVariables) && (
|
|
||||||
<Alert
|
|
||||||
title="This public dashboard may not work since it uses template variables"
|
|
||||||
severity="warning"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Alert title="You don't have permissions to create or update a public dashboard" severity="warning" />
|
|
||||||
)}
|
|
||||||
<HorizontalGroup>
|
|
||||||
<Layout orientation={isDesktop ? 0 : 1}>
|
|
||||||
<Button type="submit" disabled={isSaveDisabled} data-testid={selectors.SaveConfigButton}>
|
|
||||||
{!!publicDashboard ? 'Save public dashboard' : 'Create public dashboard'}
|
|
||||||
</Button>
|
|
||||||
{publicDashboard && hasWritePermissions && (
|
|
||||||
<DeletePublicDashboardButton
|
|
||||||
type="button"
|
|
||||||
disabled={isDeleteDisabled}
|
|
||||||
data-testid={selectors.DeleteButton}
|
|
||||||
onDismiss={onDismissDelete}
|
|
||||||
variant="destructive"
|
|
||||||
dashboard={props.dashboard}
|
|
||||||
publicDashboard={{
|
|
||||||
uid: publicDashboard.uid,
|
|
||||||
dashboardUid: props.dashboard.uid,
|
|
||||||
title: props.dashboard.title,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete public dashboard
|
|
||||||
</DeletePublicDashboardButton>
|
|
||||||
)}
|
|
||||||
</Layout>
|
|
||||||
{(isSaveLoading || isFetching) && <Spinner />}
|
|
||||||
</HorizontalGroup>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
content: css`
|
loadingContainer: css`
|
||||||
margin: ${theme.spacing(1, 0, 0, 0)};
|
height: 280px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: ${theme.spacing(1)};
|
||||||
`,
|
`,
|
||||||
checkboxes: css`
|
spinner: css`
|
||||||
margin: ${theme.spacing(2, 0)};
|
margin-bottom: ${theme.spacing(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,24 +1,21 @@
|
|||||||
import { getConfig } from 'app/core/config';
|
import { getConfig } from 'app/core/config';
|
||||||
import { VariableModel } from 'app/features/variables/types';
|
import { VariableModel } from 'app/features/variables/types';
|
||||||
import { DashboardDataDTO, DashboardMeta } from 'app/types/dashboard';
|
|
||||||
|
|
||||||
import { PanelModel } from '../../../state';
|
import { PanelModel } from '../../../state';
|
||||||
|
|
||||||
import { supportedDatasources } from './SupportedPubdashDatasources';
|
import { supportedDatasources } from './SupportedPubdashDatasources';
|
||||||
|
|
||||||
export interface PublicDashboard {
|
export interface PublicDashboardSettings {
|
||||||
accessToken?: string;
|
|
||||||
annotationsEnabled: boolean;
|
annotationsEnabled: boolean;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
uid: string;
|
|
||||||
dashboardUid: string;
|
|
||||||
timeSettings?: object;
|
|
||||||
timeSelectionEnabled: boolean;
|
timeSelectionEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardResponse {
|
export interface PublicDashboard extends PublicDashboardSettings {
|
||||||
dashboard: DashboardDataDTO;
|
accessToken?: string;
|
||||||
meta: DashboardMeta;
|
uid: string;
|
||||||
|
dashboardUid: string;
|
||||||
|
timeSettings?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance methods
|
// Instance methods
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export const DeletePublicDashboardButton = ({
|
|||||||
<ModalsController>
|
<ModalsController>
|
||||||
{({ showModal, hideModal }) => (
|
{({ showModal, hideModal }) => (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Delete public dashboard"
|
aria-label="Revoke public URL"
|
||||||
title="Delete public dashboard"
|
title="Revoke public URL"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
showModal(DeletePublicDashboardModal, {
|
showModal(DeletePublicDashboardModal, {
|
||||||
dashboardTitle: publicDashboard.title,
|
dashboardTitle: publicDashboard.title,
|
||||||
|
|||||||
Reference in New Issue
Block a user