diff --git a/e2e/dashboards-suite/dashboard-public-create.spec.ts b/e2e/dashboards-suite/dashboard-public-create.spec.ts index 745fcc4655a..c920cdc7022 100644 --- a/e2e/dashboards-suite/dashboard-public-create.spec.ts +++ b/e2e/dashboards-suite/dashboard-public-create.spec.ts @@ -18,32 +18,42 @@ e2e.scenario({ // Select public dashboards tab e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click(); - // Saving button should be disabled - e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.disabled'); + // Create button 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 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.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 - e2e.pages.ShareDashboardModal.PublicDashboard.EnableSwitch().should('be.enabled').click({ force: true }); - e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.enabled'); - - // Save public dashboard + // Create public dashboard 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'); - // Checkboxes should be disabled after saving public dashboard - e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('be.disabled'); - e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('be.disabled'); - e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('be.disabled'); + // These elements shouldn't be rendered after creating public dashboard + e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('not.exist'); + e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('not.exist'); + e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('not.exist'); + e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('not.exist'); - // Save public dashboard button should still be enabled - e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.enabled'); + // These elements should be rendered + 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().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 e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput() @@ -106,29 +121,20 @@ e2e.scenario({ e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click(); 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 e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput() .invoke('val') .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 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'); // 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 e2e() diff --git a/e2e/dashboards-suite/dashboard-public-templating.spec.ts b/e2e/dashboards-suite/dashboard-public-templating.spec.ts index 808cff56902..0c527562732 100644 --- a/e2e/dashboards-suite/dashboard-public-templating.spec.ts +++ b/e2e/dashboards-suite/dashboard-public-templating.spec.ts @@ -1,8 +1,8 @@ import { e2e } from '@grafana/e2e'; e2e.scenario({ - describeName: 'Create a public dashboard with template variables is disabled', - itName: '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 shows a template variable warning', addScenarioDataSource: false, addScenarioDashBoard: false, skipScenario: false, @@ -20,10 +20,11 @@ e2e.scenario({ e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible'); // Configuration elements for public dashboards should not exist - e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('not.exist'); - e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('not.exist'); - e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('not.exist'); - e2e.pages.ShareDashboardModal.PublicDashboard.EnableSwitch().should('not.exist'); - e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('not.exist'); + e2e.pages.ShareDashboardModal.PublicDashboard.WillBePublicCheckbox().should('exist'); + e2e.pages.ShareDashboardModal.PublicDashboard.LimitedDSCheckbox().should('exist'); + e2e.pages.ShareDashboardModal.PublicDashboard.CostIncreaseCheckbox().should('exist'); + e2e.pages.ShareDashboardModal.PublicDashboard.CreateButton().should('exist'); + + e2e.pages.ShareDashboardModal.PublicDashboard.PauseSwitch().should('not.exist'); }, }); diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 5aaabef9490..de149ca2e6a 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -190,14 +190,15 @@ export const Pages = { WillBePublicCheckbox: 'data-testid public dashboard will be public checkbox', LimitedDSCheckbox: 'data-testid public dashboard limited datasources 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', - SaveConfigButton: 'data-testid public dashboard save config button', + CreateButton: 'data-testid public dashboard create button', DeleteButton: 'data-testid public dashboard delete button', CopyUrlInput: 'data-testid public dashboard copy url input', CopyUrlButton: 'data-testid public dashboard copy url button', 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', }, }, diff --git a/packages/grafana-ui/src/components/Forms/Checkbox.tsx b/packages/grafana-ui/src/components/Forms/Checkbox.tsx index c5e16941e0f..0d0b708d251 100644 --- a/packages/grafana-ui/src/components/Forms/Checkbox.tsx +++ b/packages/grafana-ui/src/components/Forms/Checkbox.tsx @@ -30,7 +30,7 @@ export const Checkbox = React.forwardRef( return ( diff --git a/public/app/features/dashboard/api/publicDashboardApi.ts b/public/app/features/dashboard/api/publicDashboardApi.ts index 4c3a67f3071..b4cd1508e8a 100644 --- a/public/app/features/dashboard/api/publicDashboardApi.ts +++ b/public/app/features/dashboard/api/publicDashboardApi.ts @@ -4,7 +4,10 @@ import { lastValueFrom } from 'rxjs'; import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime/src'; import { notifyApp } from 'app/core/actions'; 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 { ListPublicDashboardResponse } from 'app/features/manage-dashboards/types'; @@ -55,7 +58,10 @@ export const publicDashboardApi = createApi({ }, providesTags: (result, error, dashboardUid) => [{ type: 'PublicDashboard', id: dashboardUid }], }), - createPublicDashboard: builder.mutation({ + createPublicDashboard: builder.mutation< + PublicDashboard, + { dashboard: DashboardModel; payload: Partial } + >({ query: (params) => ({ url: `/uid/${params.dashboard.uid}/public-dashboards`, method: 'POST', @@ -63,7 +69,7 @@ export const publicDashboardApi = createApi({ }), async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) { const { data } = await queryFulfilled; - dispatch(notifyApp(createSuccessNotification('Public dashboard created!'))); + dispatch(notifyApp(createSuccessNotification('Dashboard is public!'))); // Update runtime meta flag dashboard.updateMeta({ @@ -71,7 +77,7 @@ export const publicDashboardApi = createApi({ publicDashboardEnabled: data.isEnabled, }); }, - invalidatesTags: (result, error, { payload }) => [{ type: 'PublicDashboard', id: payload.dashboardUid }], + invalidatesTags: (result, error, { dashboard }) => [{ type: 'PublicDashboard', id: dashboard.uid }], }), updatePublicDashboard: builder.mutation({ query: (params) => ({ diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes.tsx deleted file mode 100644 index 3d010865c96..00000000000 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes.tsx +++ /dev/null @@ -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; -}) => ( - <> -

Before you click Save, please acknowledge the following information:

-
- - {ACKNOWLEDGES.map((acknowledge) => ( - - - - - ))} - -
- -); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx new file mode 100644 index 00000000000..0a7d0e16f2a --- /dev/null +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx @@ -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({ + 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 ( +
+ {hasWritePermissions && dashboard.hasUnsavedChanges() && } + {!hasWritePermissions && } + {dashboardHasTemplateVariables(dashboardVariables) && } + {!!unsupportedDataSources.length && ( + + )} +
+ +

Settings

+ {(isUpdateLoading || isGetLoading) && } +
+
+ +
+ + generatePublicDashboardUrl(publicDashboard!)} + > + Copy + + } + /> + + + + { + reportInteraction('grafana_dashboards_public_enable_clicked', { + action: e.currentTarget.checked ? 'disable' : 'enable', + }); + onChange('isPaused', e.currentTarget.checked); + }} + data-testid={selectors.PauseSwitch} + /> + + + + + Revoke public URL + + + +
+ ); +}; + +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; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/Configuration.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/Configuration.tsx similarity index 51% rename from public/app/features/dashboard/components/ShareModal/SharePublicDashboard/Configuration.tsx rename to public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/Configuration.tsx index e6447e1e93e..7ae2260b41b 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/Configuration.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/Configuration.tsx @@ -7,69 +7,60 @@ import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { reportInteraction } from '@grafana/runtime/src'; import { FieldSet, Label, Switch, TimeRangeInput, useStyles2, VerticalGroup } from '@grafana/ui/src'; import { Layout } from '@grafana/ui/src/components/Layout/Layout'; -import { DashboardModel } from 'app/features/dashboard/state'; -import { useIsDesktop } from 'app/features/dashboard/utils/screen'; import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; -import { SharePublicDashboardInputs } from './SharePublicDashboard'; +import { useSelector } from '../../../../../../types'; + +import { ConfigPublicDashoardForm } from './ConfigPublicDashboard'; + +const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard; export const Configuration = ({ disabled, - dashboard, + onChange, register, }: { disabled: boolean; - dashboard: DashboardModel; - register: UseFormRegister; + onChange: (name: keyof ConfigPublicDashoardForm, value: boolean) => void; + register: UseFormRegister; }) => { - const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard; const styles = useStyles2(getStyles); - const isDesktop = useIsDesktop(); + + const dashboardState = useSelector((store) => store.dashboard); + const dashboard = dashboardState.getModel()!; const timeRange = getTimeRange(dashboard.getDefaultTime(), dashboard); return ( <> -

Public dashboard configuration

- -
@@ -78,16 +69,7 @@ export const Configuration = ({ }; const getStyles = (theme: GrafanaTheme2) => ({ - title: css` - margin-bottom: ${theme.spacing(2)}; - `, dashboardConfig: css` margin: ${theme.spacing(0, 0, 3, 0)}; `, - timeRange: css` - margin-bottom: ${theme.spacing(0)}; - `, - timeRangeDisabledText: css` - font-size: ${theme.typography.bodySmall.fontSize}; - `, }); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/AcknowledgeCheckboxes.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/AcknowledgeCheckboxes.tsx new file mode 100644 index 00000000000..46c1f8f396b --- /dev/null +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/AcknowledgeCheckboxes.tsx @@ -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; +}) => { + const styles = useStyles2(getStyles); + + return ( + <> +

Before you make the dashboard public, acknowledge the following:

+
+ + {ACKNOWLEDGES.map((acknowledge) => ( + + + + + ))} + +
+ + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + title: css` + font-weight: ${theme.typography.fontWeightBold}; + `, +}); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx new file mode 100644 index 00000000000..4814e40622c --- /dev/null +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx @@ -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 ( +
+

Welcome to public dashboards alpha!

+ + {!hasWritePermissions && } + {dashboardHasTemplateVariables(dashboard.getVariables()) && } + {!!unsupportedDataSources.length && ( + + )} +
+ {({ + register, + formState: { isValid }, + }: { + register: UseFormRegister; + formState: FormState; + }) => ( + <> +
+ +
+
+ +
+ + )} + +
+ ); +}; + +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; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/Description.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/Description.tsx new file mode 100644 index 00000000000..1041e8ccee2 --- /dev/null +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/Description.tsx @@ -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 ( +
+

Currently, we don’t support template variables or frontend data sources

+

+ We'd love your feedback. To share, please comment on this{' '} + + GitHub discussion + + . +

+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css` + margin-bottom: ${theme.spacing(3)}; + `, + description: css` + color: ${theme.colors.text.secondary}; + margin-bottom: ${theme.spacing(1)}; + `, +}); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/Description.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/Description.tsx deleted file mode 100644 index 0fdd76aac65..00000000000 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/Description.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -export const Description = () => ( - <> -

- To allow the current dashboard to be published publicly, toggle the switch. For now we do not support template - variables or frontend datasources. -

-

- We'd love your feedback. To share, please comment on this{' '} - - GitHub discussion - - . -

- -); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/NoUpsertPermissionsAlert.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/NoUpsertPermissionsAlert.tsx new file mode 100644 index 00000000000..d0245f93cb6 --- /dev/null +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/NoUpsertPermissionsAlert.tsx @@ -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' }) => ( + + Contact your admin to get permission to {mode} create public dashboards + +); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/SaveDashboardChangesAlert.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/SaveDashboardChangesAlert.tsx new file mode 100644 index 00000000000..2a88bce977d --- /dev/null +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/SaveDashboardChangesAlert.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +import { Alert } from '@grafana/ui/src'; + +export const SaveDashboardChangesAlert = () => ( + +); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/UnsupportedDataSourcesAlert.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/UnsupportedDataSourcesAlert.tsx new file mode 100644 index 00000000000..615ccc64353 --- /dev/null +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/UnsupportedDataSourcesAlert.tsx @@ -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 ( + +

+ There are data sources in this dashboard that are unsupported for public dashboards. Panels that use these data + sources may not function properly: {unsupportedDataSources}. +

+ + Read more about supported data sources + +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + unsupportedDataSourceDescription: css` + color: ${theme.colors.text.secondary}; + margin-bottom: ${theme.spacing(1)}; + `, +}); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/UnsupportedTemplateVariablesAlert.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/UnsupportedTemplateVariablesAlert.tsx new file mode 100644 index 00000000000..07da2bc1a76 --- /dev/null +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/UnsupportedTemplateVariablesAlert.tsx @@ -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 = () => ( + + This public dashboard may not work since it uses template variables + +); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx index 76d03e49d95..fd38c03bbbd 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx @@ -17,9 +17,10 @@ import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures'; import { configureStore } from 'app/store/configureStore'; +import { DashboardInitPhase } from '../../../../../types'; import { ShareModal } from '../ShareModal'; -import { PublicDashboard } from './SharePublicDashboardUtils'; +import * as sharePublicDashboardUtils from './SharePublicDashboardUtils'; const server = setupServer(); @@ -28,17 +29,39 @@ jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => backendSrv, })); -const renderSharePublicDashboard = async (props: React.ComponentProps, isEnabled = true) => { - const store = configureStore(); +const renderSharePublicDashboard = async ( + props?: Partial>, + 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( - + ); 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; @@ -97,139 +120,54 @@ afterEach(() => { server.resetHandlers(); }); -describe('SharePublic', () => { - const pubdashResponse: PublicDashboard = { - isEnabled: true, - annotationsEnabled: true, - timeSelectionEnabled: true, - uid: 'a-uid', - dashboardUid: '', - accessToken: 'an-access-token', - }; +const pubdashResponse: sharePublicDashboardUtils.PublicDashboard = { + isEnabled: true, + annotationsEnabled: true, + timeSelectionEnabled: true, + uid: 'a-uid', + dashboardUid: '', + accessToken: 'an-access-token', +}; - beforeEach(() => { - server.use( - rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => { - return res( - ctx.status(200), - ctx.json({ - ...pubdashResponse, - dashboardUid: req.params.dashboardUid, - }) - ); +const getExistentPublicDashboardResponse = () => + rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + ...pubdashResponse, + dashboardUid: req.params.dashboardUid, }) ); }); - - it('does not render share panel when public dashboards feature is disabled', async () => { - config.featureToggles.publicDashboards = false; - await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }, false); - - expect(screen.getByRole('tablist')).toHaveTextContent('Link'); - expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard'); +const getNonExistentPublicDashboardResponse = () => + 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('renders share panel when public dashboards feature is enabled', async () => { - await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); - - expect(screen.getByRole('tablist')).toHaveTextContent('Link'); - expect(screen.getByRole('tablist')).toHaveTextContent('Public dashboard'); - - fireEvent.click(screen.getByText('Public dashboard')); - - await screen.findByText('Welcome to Grafana public dashboards alpha!'); - expect(screen.getByText('Create public dashboard')).toBeInTheDocument(); - expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument(); +const getErrorPublicDashboardResponse = () => + rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => { + return res(ctx.status(500)); }); - 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); - await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); - expect(screen.getByRole('tablist')).toHaveTextContent('Link'); - expect(screen.getByRole('tablist')).toHaveTextContent('Public dashboard'); - - fireEvent.click(screen.getByText('Public dashboard')); - - await screen.findByText('Welcome to Grafana public dashboards alpha!'); - - expect(screen.getByText('Create public dashboard')).toBeInTheDocument(); - expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument(); + await renderSharePublicDashboard(); + expect(screen.queryByTestId(selectors.NoUpsertPermissionsWarningAlert)).toBeInTheDocument(); }); - it('renders default relative time in input', async () => { - expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' }); + it('when dashboard has template variables, warning is shown', async () => { + jest.spyOn(sharePublicDashboardUtils, 'dashboardHasTemplateVariables').mockReturnValue(true); - //@ts-ignore - mockDashboard.originalTime = { from: 'now-6h', to: 'now' }; - - await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); - - await screen.findByText('Welcome to Grafana public dashboards alpha!'); - expect(screen.getByText('Last 6 hours')).toBeInTheDocument(); - }); - it('renders default absolute time in input 2', async () => { - mockDashboard.time = { from: '2022-08-30T03:00:00.000Z', to: '2022-09-04T02:59:59.000Z' }; - //@ts-ignore - mockDashboard.originalTime = { from: '2022-08-30T06:00:00.000Z', to: '2022-09-04T06:59:59.000Z' }; - - await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); - - await screen.findByText('Welcome to Grafana public dashboards alpha!'); - expect(screen.getByText('2022-08-30 00:00:00 to 2022-09-04 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(); + await renderSharePublicDashboard(); + expect(screen.queryByTestId(selectors.TemplateVariablesWarningAlert)).toBeInTheDocument(); }); it('when dashboard has unsupported datasources, warning is shown', async () => { const panelModel = { @@ -244,108 +182,136 @@ describe('SharePublic - New config setup', () => { panels: [panelModel], }); - await renderSharePublicDashboard({ panel: mockPanel, dashboard, onDismiss: () => {} }); - - expect(screen.queryByTestId(selectors.UnsupportedDatasourcesWarningAlert)).toBeInTheDocument(); + await renderSharePublicDashboard({ dashboard }); + 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.LimitedDSCheckbox)).toBeEnabled(); expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeEnabled(); - expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled(); - expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled(); - expect(screen.getByTestId(selectors.EnableTimeRangeSwitch)).toBeEnabled(); + expect(screen.getByTestId(selectors.CreateButton)).toBeDisabled(); 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 () => { - await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); - await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')); + it('when fetch errors happen, then all inputs remain disabled', async () => { + server.use(getErrorPublicDashboardResponse()); - fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox)); - fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox)); - fireEvent.click(screen.getByTestId(selectors.CostIncreaseCheckbox)); + await renderSharePublicDashboard(); - expect(screen.getByText('Create public dashboard')).toBeInTheDocument(); - expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled(); - }); - it('when checkboxes and switch are filled, then save button is enabled', async () => { - await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); - await waitForElementToBeRemoved(screen.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(); + expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled(); + expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled(); + expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled(); + expect(screen.getByTestId(selectors.CreateButton)).toBeDisabled(); + expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument(); }); }); -describe('SharePublic - Already persisted', () => { - const pubdashResponse: PublicDashboard = { - isEnabled: true, - annotationsEnabled: true, - timeSelectionEnabled: true, - uid: 'a-uid', - dashboardUid: '', - accessToken: 'an-access-token', - }; - +describe('SharePublic - New config setup', () => { beforeEach(() => { - server.use( - rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => { - return res( - ctx.status(200), - ctx.json({ - ...pubdashResponse, - dashboardUid: req.params.dashboardUid, - }) - ); - }) - ); + server.use(getNonExistentPublicDashboardResponse()); }); - it('when modal is opened, then save button and delete button are enabled', async () => { - await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); - await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')); + it('renders when public dashboards feature is enabled', async () => { + await renderSharePublicDashboard(); - expect(screen.getByTestId(selectors.DeleteButton)).toBeEnabled(); - expect(screen.getByText('Save public dashboard')).toBeInTheDocument(); - expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled(); - }); - it('delete button is not rendered because lack of permissions', async () => { - jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false); - await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); - await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')); + await screen.findByText('Welcome to public dashboards alpha!'); + expect(screen.getByText('Generate public URL')).toBeInTheDocument(); + + expect(screen.queryByTestId(selectors.WillBePublicCheckbox)).toBeInTheDocument(); + expect(screen.queryByTestId(selectors.LimitedDSCheckbox)).toBeInTheDocument(); + expect(screen.queryByTestId(selectors.CostIncreaseCheckbox)).toBeInTheDocument(); + expect(screen.queryByTestId(selectors.CreateButton)).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 () => { - await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); + it('when modal is opened, then create button is disabled', async () => { + 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')); + 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)).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(enableTimeRangeSwitch).toBeEnabled(); - expect(enableTimeRangeSwitch).toBeChecked(); - }); + expect(screen.getByTestId(selectors.PauseSwitch)).toBeEnabled(); + expect(screen.getByTestId(selectors.PauseSwitch)).not.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 () => { server.use( 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')); const enableTimeRangeSwitch = screen.getByTestId(selectors.EnableTimeRangeSwitch); expect(enableTimeRangeSwitch).toBeEnabled(); 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 () => { - await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); - await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')); + await renderSharePublicDashboard(); 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( rest.get('/api/dashboards/uid/:dashboardUid/public-dashboards', (req, res, ctx) => { return res( @@ -401,22 +352,13 @@ describe('SharePublic - Already persisted', () => { }) ); - await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); + await renderSharePublicDashboard(); await waitForElementToBeRemoved(screen.getAllByTestId('Spinner')); - expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument(); - expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).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')); + expect(screen.queryByTestId(selectors.CopyUrlInput)).toBeInTheDocument(); + expect(screen.queryByTestId(selectors.CopyUrlButton)).not.toBeChecked(); - fireEvent.click(screen.getByTestId(selectors.EnableSwitch)); - 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(); + expect(screen.getByTestId(selectors.PauseSwitch)).toBeChecked(); }); + alertTests(); }); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx index 270671fe812..569ce8f0331 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx @@ -1,307 +1,60 @@ import { css } from '@emotion/css'; -import React, { useContext, useEffect, useMemo } from 'react'; -import { useForm } from 'react-hook-form'; -import { Subscription } from 'rxjs'; +import React, { useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data/src'; -import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { reportInteraction } from '@grafana/runtime/src'; -import { - Alert, - Button, - ClipboardButton, - Field, - HorizontalGroup, - Input, - useStyles2, - Spinner, - 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 { Spinner, useStyles2 } from '@grafana/ui/src'; +import { useGetPublicDashboardQuery } from 'app/features/dashboard/api/publicDashboardApi'; +import { publicDashboardPersisted } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; 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 { ShareModal } from '../ShareModal'; +import { HorizontalGroup } from '../../../../plugins/admin/components/HorizontalGroup'; +import ConfigPublicDashboard from './ConfigPublicDashboard/ConfigPublicDashboard'; +import CreatePublicDashboard from './CreatePublicDashboard/CreatePublicDashboard'; interface Props extends ShareModalTabProps {} -export type SharePublicDashboardAcknowledgmentInputs = { - publicAcknowledgment: boolean; - dataSourcesAcknowledgment: boolean; - usageAcknowledgment: boolean; +const Loader = () => { + const styles = useStyles2(getStyles); + + return ( + + <> + Loading configuration + + + + ); }; -export type SharePublicDashboardInputs = { - isAnnotationsEnabled: boolean; - enabledSwitch: boolean; - isTimeRangeEnabled: boolean; -} & SharePublicDashboardAcknowledgmentInputs; - export const SharePublicDashboard = (props: Props) => { - const forceUpdate = useForceUpdate(); - 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({ - defaultValues: { - publicAcknowledgment: false, - dataSourcesAcknowledgment: false, - usageAcknowledgment: false, - isAnnotationsEnabled: false, - isTimeRangeEnabled: false, - enabledSwitch: false, - }, - }); - - const [createPublicDashboard, { isLoading: isSaveLoading }] = useCreatePublicDashboardMutation(); - const [updatePublicDashboard, { isLoading: isUpdateLoading }] = useUpdatePublicDashboardMutation(); + const { data: publicDashboard, isLoading, isError } = useGetPublicDashboardQuery(props.dashboard.uid); useEffect(() => { - const eventSubs = new Subscription(); - eventSubs.add(props.dashboard.events.subscribe(DashboardMetaChangedEvent, forceUpdate)); 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 ( <> - -

- Welcome to Grafana public dashboards alpha! -

- {(isGetLoading || isFetching) && } -
-
- {getUnsupportedDashboardDatasources(props.dashboard.panels).length > 0 ? ( - -
- {`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 `} - - docs - {' '} - for supported datasources. -
-
- ) : null} - {dashboardHasTemplateVariables(dashboardVariables) && !publicDashboardPersisted(publicDashboard) ? ( - - This dashboard cannot be made public because it has template variables - - ) : ( -
- -
-
- -
-
- - {publicDashboardPersisted(publicDashboard) && watch('enabledSwitch') && ( - - generatePublicDashboardUrl(publicDashboard!)} - > - Copy - - } - /> - - )} - {hasWritePermissions ? ( - props.dashboard.hasUnsavedChanges() ? ( - - ) : ( - dashboardHasTemplateVariables(dashboardVariables) && ( - - ) - ) - ) : ( - - )} - - - - {publicDashboard && hasWritePermissions && ( - - Delete public dashboard - - )} - - {(isSaveLoading || isFetching) && } - - - )} -
+ {isLoading ? ( + + ) : !publicDashboardPersisted(publicDashboard) ? ( + + ) : ( + + )} ); }; const getStyles = (theme: GrafanaTheme2) => ({ - content: css` - margin: ${theme.spacing(1, 0, 0, 0)}; + loadingContainer: css` + height: 280px; + align-items: center; + justify-content: center; + gap: ${theme.spacing(1)}; `, - checkboxes: css` - margin: ${theme.spacing(2, 0)}; - `, - timeRange: css` - padding: ${theme.spacing(1, 1)}; - margin: ${theme.spacing(0, 0, 2, 0)}; - `, - publicUrl: css` - width: 100%; - margin-bottom: ${theme.spacing(0, 0, 3, 0)}; + spinner: css` + margin-bottom: ${theme.spacing(0)}; `, }); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils.ts b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils.ts index 17ef31f3ee1..cc732f8ed13 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils.ts +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils.ts @@ -1,24 +1,21 @@ import { getConfig } from 'app/core/config'; import { VariableModel } from 'app/features/variables/types'; -import { DashboardDataDTO, DashboardMeta } from 'app/types/dashboard'; import { PanelModel } from '../../../state'; import { supportedDatasources } from './SupportedPubdashDatasources'; -export interface PublicDashboard { - accessToken?: string; +export interface PublicDashboardSettings { annotationsEnabled: boolean; isEnabled: boolean; - uid: string; - dashboardUid: string; - timeSettings?: object; timeSelectionEnabled: boolean; } -export interface DashboardResponse { - dashboard: DashboardDataDTO; - meta: DashboardMeta; +export interface PublicDashboard extends PublicDashboardSettings { + accessToken?: string; + uid: string; + dashboardUid: string; + timeSettings?: object; } // Instance methods diff --git a/public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton.tsx b/public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton.tsx index 125aebe4d3e..a9b39df8cf0 100644 --- a/public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton.tsx +++ b/public/app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton.tsx @@ -41,8 +41,8 @@ export const DeletePublicDashboardButton = ({ {({ showModal, hideModal }) => (