mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PublicDashboard: Add RTK Query with loading and error state. Add MSW dependency for testing. (#55518)
PublicDashboard: Add RTK Query with loading and error state. Add MSW dependency for testing.
This commit is contained in:
parent
2f14575dd3
commit
c7419de2e5
@ -1,5 +1,5 @@
|
||||
// BETTERER RESULTS V2.
|
||||
//
|
||||
//
|
||||
// If this file contains merge conflicts, use `betterer merge` to automatically resolve them:
|
||||
// https://phenomnomnominal.github.io/betterer/docs/results-file/#merge
|
||||
//
|
||||
@ -3782,7 +3782,7 @@ exports[`better eslint`] = {
|
||||
"public/app/features/dashboard/components/ShareModal/ShareModal.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard/components/ShareModal/SharePublicDashboard.test.tsx:5381": [
|
||||
"public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx:5381": [
|
||||
|
@ -28,8 +28,11 @@ e2e.scenario({
|
||||
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');
|
||||
|
||||
// Switch on enabling toggle
|
||||
e2e.pages.ShareDashboardModal.PublicDashboard.EnableSwitch().should('be.enabled').click({ force: true });
|
||||
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.enabled');
|
||||
|
||||
// Save configuration
|
||||
e2e().intercept('POST', '/api/dashboards/uid/ZqZnVvFZz/public-config').as('save');
|
||||
@ -69,6 +72,8 @@ e2e.scenario({
|
||||
e2e.pages.ShareDashboardModal.PublicDashboard.Tab().click();
|
||||
e2e().wait('@query-public-config');
|
||||
|
||||
e2e.pages.ShareDashboardModal.PublicDashboard.SaveConfigButton().should('be.enabled');
|
||||
|
||||
// Make a request to public dashboards api endpoint without authentication
|
||||
e2e.pages.ShareDashboardModal.PublicDashboard.CopyUrlInput()
|
||||
.invoke('val')
|
||||
|
@ -207,6 +207,7 @@
|
||||
"lerna": "5.2.0",
|
||||
"lint-staged": "13.0.3",
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"msw": "^0.47.3",
|
||||
"mutationobserver-shim": "0.3.7",
|
||||
"ngtemplate-loader": "2.1.0",
|
||||
"node-notifier": "10.0.1",
|
||||
|
@ -4,6 +4,7 @@ import sharedReducers from 'app/core/reducers';
|
||||
import ldapReducers from 'app/features/admin/state/reducers';
|
||||
import alertingReducers from 'app/features/alerting/state/reducers';
|
||||
import apiKeysReducers from 'app/features/api-keys/state/reducers';
|
||||
import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi';
|
||||
import panelEditorReducers from 'app/features/dashboard/components/PanelEditor/state/reducers';
|
||||
import dashboardReducers from 'app/features/dashboard/state/reducers';
|
||||
import dataSourcesReducers from 'app/features/datasources/state/reducers';
|
||||
@ -46,6 +47,7 @@ const rootReducers = {
|
||||
...searchQueryReducer,
|
||||
plugins: pluginsReducer,
|
||||
[alertingApi.reducerPath]: alertingApi.reducer,
|
||||
[publicDashboardApi.reducerPath]: publicDashboardApi.reducer,
|
||||
};
|
||||
|
||||
const addedReducers = {};
|
||||
|
56
public/app/features/dashboard/api/publicDashboardApi.ts
Normal file
56
public/app/features/dashboard/api/publicDashboardApi.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { BaseQueryFn, createApi, retry } from '@reduxjs/toolkit/query/react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime/src';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
import { PublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
|
||||
const backendSrvBaseQuery =
|
||||
({ baseUrl }: { baseUrl: string } = { baseUrl: '' }): BaseQueryFn<BackendSrvRequest> =>
|
||||
async (requestOptions) => {
|
||||
try {
|
||||
const { data: responseData, ...meta } = await lastValueFrom(
|
||||
getBackendSrv().fetch({ ...requestOptions, url: baseUrl + requestOptions.url })
|
||||
);
|
||||
return { data: responseData, meta };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
|
||||
export const publicDashboardApi = createApi({
|
||||
reducerPath: 'publicDashboardApi',
|
||||
baseQuery: retry(backendSrvBaseQuery({ baseUrl: '/api/dashboards' }), { maxRetries: 3 }),
|
||||
tagTypes: ['Config'],
|
||||
keepUnusedDataFor: 0,
|
||||
endpoints: (builder) => ({
|
||||
getConfig: builder.query<PublicDashboard, string>({
|
||||
query: (dashboardUid) => ({
|
||||
url: `/uid/${dashboardUid}/public-config`,
|
||||
}),
|
||||
providesTags: ['Config'],
|
||||
}),
|
||||
saveConfig: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
|
||||
query: (params) => ({
|
||||
url: `/uid/${params.dashboard.uid}/public-config`,
|
||||
method: 'POST',
|
||||
data: params.payload,
|
||||
}),
|
||||
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
|
||||
const { data } = await queryFulfilled;
|
||||
dispatch(notifyApp(createSuccessNotification('Dashboard sharing configuration saved')));
|
||||
|
||||
// Update runtime meta flag
|
||||
dashboard.updateMeta({
|
||||
publicDashboardUid: data.uid,
|
||||
publicDashboardEnabled: data.isEnabled,
|
||||
});
|
||||
},
|
||||
invalidatesTags: ['Config'],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetConfigQuery, useSaveConfigMutation } = publicDashboardApi;
|
@ -5,6 +5,7 @@ import { reportInteraction } from '@grafana/runtime/src';
|
||||
import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { SharePublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard';
|
||||
|
||||
@ -12,7 +13,6 @@ import { ShareEmbed } from './ShareEmbed';
|
||||
import { ShareExport } from './ShareExport';
|
||||
import { ShareLibraryPanel } from './ShareLibraryPanel';
|
||||
import { ShareLink } from './ShareLink';
|
||||
import { SharePublicDashboard } from './SharePublicDashboard';
|
||||
import { ShareSnapshot } from './ShareSnapshot';
|
||||
import { ShareModalTabModel } from './types';
|
||||
|
||||
|
@ -1,146 +0,0 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { BootData } from '@grafana/data';
|
||||
import { BackendSrv, setEchoSrv } from '@grafana/runtime';
|
||||
import config from 'app/core/config';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
|
||||
import { Echo } from '../../../../core/services/echo/Echo';
|
||||
|
||||
import { ShareModal } from './ShareModal';
|
||||
import { PublicDashboard } from './SharePublicDashboardUtils';
|
||||
|
||||
// Mock api request
|
||||
const publicDashboardconfigResp: PublicDashboard = {
|
||||
isEnabled: true,
|
||||
uid: '',
|
||||
dashboardUid: '',
|
||||
accessToken: '',
|
||||
};
|
||||
|
||||
const backendSrv = {
|
||||
get: () => publicDashboardconfigResp,
|
||||
} as unknown as BackendSrv;
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
|
||||
jest.mock('app/core/core', () => {
|
||||
return {
|
||||
contextSrv: {
|
||||
hasPermission: () => true,
|
||||
},
|
||||
appEvents: {
|
||||
subscribe: () => {
|
||||
return {
|
||||
unsubscribe: () => {},
|
||||
};
|
||||
},
|
||||
emit: () => {},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('SharePublic', () => {
|
||||
let originalBootData: BootData;
|
||||
|
||||
beforeAll(() => {
|
||||
setEchoSrv(new Echo());
|
||||
originalBootData = config.bootData;
|
||||
config.appUrl = 'http://dashboards.grafana.com/';
|
||||
|
||||
config.bootData = {
|
||||
user: {
|
||||
orgId: 1,
|
||||
},
|
||||
} as any;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.bootData = originalBootData;
|
||||
});
|
||||
|
||||
it('does not render share panel when public dashboards feature is disabled', () => {
|
||||
const mockDashboard = new DashboardModel({
|
||||
uid: 'mockDashboardUid',
|
||||
});
|
||||
const mockPanel = new PanelModel({
|
||||
id: 'mockPanelId',
|
||||
});
|
||||
|
||||
render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />);
|
||||
|
||||
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
||||
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard');
|
||||
});
|
||||
|
||||
it('renders share panel when public dashboards feature is enabled', async () => {
|
||||
config.featureToggles.publicDashboards = true;
|
||||
const mockDashboard = new DashboardModel({
|
||||
uid: 'mockDashboardUid',
|
||||
});
|
||||
const mockPanel = new PanelModel({
|
||||
id: 'mockPanelId',
|
||||
});
|
||||
|
||||
render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />);
|
||||
|
||||
await waitFor(() => screen.getByText('Link'));
|
||||
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
||||
expect(screen.getByRole('tablist')).toHaveTextContent('Public dashboard');
|
||||
|
||||
fireEvent.click(screen.getByText('Public dashboard'));
|
||||
|
||||
await screen.findByText('Welcome to Grafana public dashboards alpha!');
|
||||
});
|
||||
|
||||
it('renders default relative time in input', async () => {
|
||||
config.featureToggles.publicDashboards = true;
|
||||
const mockDashboard = new DashboardModel({
|
||||
uid: 'mockDashboardUid',
|
||||
});
|
||||
const mockPanel = new PanelModel({
|
||||
id: 'mockPanelId',
|
||||
});
|
||||
|
||||
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
|
||||
|
||||
//@ts-ignore
|
||||
mockDashboard.originalTime = { from: 'now-6h', to: 'now' };
|
||||
|
||||
render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />);
|
||||
|
||||
await waitFor(() => screen.getByText('Link'));
|
||||
fireEvent.click(screen.getByText('Public dashboard'));
|
||||
|
||||
await screen.findByText('Welcome to Grafana public dashboards alpha!');
|
||||
expect(screen.getByText('Last 6 hours')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders default absolute time in input 2', async () => {
|
||||
config.featureToggles.publicDashboards = true;
|
||||
const mockDashboard = new DashboardModel({
|
||||
uid: 'mockDashboardUid',
|
||||
});
|
||||
const mockPanel = new PanelModel({
|
||||
id: 'mockPanelId',
|
||||
});
|
||||
|
||||
mockDashboard.time = { from: '2022-08-30T03:00:00.000Z', to: '2022-09-04T02:59:59.000Z' };
|
||||
//@ts-ignore
|
||||
mockDashboard.originalTime = { from: '2022-08-30T06:00:00.000Z', to: '2022-09-04T06:59:59.000Z' };
|
||||
|
||||
render(<ShareModal panel={mockPanel} dashboard={mockDashboard} onDismiss={() => {}} />);
|
||||
|
||||
await waitFor(() => screen.getByText('Link'));
|
||||
fireEvent.click(screen.getByText('Public dashboard'));
|
||||
|
||||
await screen.findByText('Welcome to Grafana public dashboards alpha!');
|
||||
expect(screen.getByText('2022-08-30 00:00:00 to 2022-09-04 01:59:59')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// test checking if current version of dashboard in state is persisted to db
|
||||
});
|
@ -1,286 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||
import { reportInteraction } from '@grafana/runtime/src';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
ClipboardButton,
|
||||
Field,
|
||||
HorizontalGroup,
|
||||
FieldSet,
|
||||
Input,
|
||||
Label,
|
||||
LinkButton,
|
||||
Switch,
|
||||
TimeRangeInput,
|
||||
useStyles2,
|
||||
VerticalGroup,
|
||||
} from '@grafana/ui';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
|
||||
import { dispatch } from 'app/store/store';
|
||||
|
||||
import { contextSrv } from '../../../../core/services/context_srv';
|
||||
import { AccessControlAction } from '../../../../types';
|
||||
import { isOrgAdmin } from '../../../plugins/admin/permissions';
|
||||
|
||||
import {
|
||||
dashboardHasTemplateVariables,
|
||||
generatePublicDashboardUrl,
|
||||
getPublicDashboardConfig,
|
||||
PublicDashboard,
|
||||
publicDashboardPersisted,
|
||||
savePublicDashboardConfig,
|
||||
} from './SharePublicDashboardUtils';
|
||||
import { ShareModalTabProps } from './types';
|
||||
|
||||
interface Props extends ShareModalTabProps {}
|
||||
|
||||
interface Acknowledgements {
|
||||
public: boolean;
|
||||
datasources: boolean;
|
||||
usage: boolean;
|
||||
}
|
||||
|
||||
export const SharePublicDashboard = (props: Props) => {
|
||||
const dashboardVariables = props.dashboard.getVariables();
|
||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
|
||||
|
||||
const [publicDashboard, setPublicDashboardConfig] = useState<PublicDashboard>({
|
||||
isEnabled: false,
|
||||
uid: '',
|
||||
dashboardUid: props.dashboard.uid,
|
||||
});
|
||||
const [acknowledgements, setAcknowledgements] = useState<Acknowledgements>({
|
||||
public: false,
|
||||
datasources: false,
|
||||
usage: false,
|
||||
});
|
||||
|
||||
const timeRange = getTimeRange(props.dashboard.getDefaultTime(), props.dashboard);
|
||||
|
||||
useEffect(() => {
|
||||
reportInteraction('grafana_dashboards_public_share_viewed');
|
||||
|
||||
getPublicDashboardConfig(props.dashboard.uid, setPublicDashboardConfig).catch();
|
||||
}, [props.dashboard.uid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (publicDashboardPersisted(publicDashboard)) {
|
||||
setAcknowledgements({
|
||||
public: true,
|
||||
datasources: true,
|
||||
usage: true,
|
||||
});
|
||||
}
|
||||
}, [publicDashboard]);
|
||||
|
||||
const onSavePublicConfig = () => {
|
||||
reportInteraction('grafana_dashboards_public_create_clicked');
|
||||
|
||||
if (dashboardHasTemplateVariables(dashboardVariables)) {
|
||||
dispatch(
|
||||
notifyApp(createErrorNotification('This dashboard cannot be made public because it has template variables'))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
savePublicDashboardConfig(props.dashboard, publicDashboard, setPublicDashboardConfig).catch();
|
||||
};
|
||||
|
||||
const onAcknowledge = useCallback(
|
||||
(field: string, checked: boolean) => {
|
||||
setAcknowledgements({ ...acknowledgements, [field]: checked });
|
||||
},
|
||||
[acknowledgements]
|
||||
);
|
||||
|
||||
// check if all conditions have been acknowledged
|
||||
const acknowledged = acknowledgements.public && acknowledgements.datasources && acknowledgements.usage;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>Welcome to Grafana public dashboards alpha!</p>
|
||||
{dashboardHasTemplateVariables(dashboardVariables) ? (
|
||||
<Alert
|
||||
severity="warning"
|
||||
title="dashboard cannot be public"
|
||||
data-testid={selectors.TemplateVariablesWarningAlert}
|
||||
>
|
||||
This dashboard cannot be made public because it has template variables
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
To allow the current dashboard to be published publicly, toggle the switch. For now we do not support
|
||||
template variables or frontend datasources.
|
||||
</p>
|
||||
<p>
|
||||
We'd love your feedback. To share, please comment on this{' '}
|
||||
<a
|
||||
href="https://github.com/grafana/grafana/discussions/49253"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-link"
|
||||
>
|
||||
GitHub discussion
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<hr />
|
||||
<div className={styles.checkboxes}>
|
||||
<p>Before you click Save, please acknowledge the following information:</p>
|
||||
<FieldSet disabled={publicDashboardPersisted(publicDashboard) || !hasWritePermissions}>
|
||||
<VerticalGroup spacing="md">
|
||||
<HorizontalGroup spacing="none">
|
||||
<Checkbox
|
||||
label="Your entire dashboard will be public"
|
||||
value={acknowledgements.public}
|
||||
data-testid={selectors.WillBePublicCheckbox}
|
||||
onChange={(e) => onAcknowledge('public', e.currentTarget.checked)}
|
||||
/>
|
||||
<LinkButton
|
||||
variant="primary"
|
||||
href="https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/"
|
||||
target="_blank"
|
||||
fill="text"
|
||||
icon="info-circle"
|
||||
rel="noopener noreferrer"
|
||||
tooltip="Learn more about public dashboards"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="none">
|
||||
<Checkbox
|
||||
label="Publishing currently only works with a subset of datasources"
|
||||
value={acknowledgements.datasources}
|
||||
data-testid={selectors.LimitedDSCheckbox}
|
||||
onChange={(e) => onAcknowledge('datasources', e.currentTarget.checked)}
|
||||
/>
|
||||
<LinkButton
|
||||
variant="primary"
|
||||
href="https://grafana.com/docs/grafana/latest/datasources/"
|
||||
target="_blank"
|
||||
fill="text"
|
||||
icon="info-circle"
|
||||
rel="noopener noreferrer"
|
||||
tooltip="Learn more about public datasources"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="none">
|
||||
<Checkbox
|
||||
label="Making your dashboard public will cause queries to run each time the dashboard is viewed which may increase costs"
|
||||
value={acknowledgements.usage}
|
||||
data-testid={selectors.CostIncreaseCheckbox}
|
||||
onChange={(e) => onAcknowledge('usage', e.currentTarget.checked)}
|
||||
/>
|
||||
<LinkButton
|
||||
variant="primary"
|
||||
href="https://grafana.com/docs/grafana/latest/enterprise/query-caching/"
|
||||
target="_blank"
|
||||
fill="text"
|
||||
icon="info-circle"
|
||||
rel="noopener noreferrer"
|
||||
tooltip="Learn more about query caching"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</FieldSet>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<h4 className="share-modal-info-text">Public dashboard configuration</h4>
|
||||
<FieldSet disabled={!hasWritePermissions} className={styles.dashboardConfig}>
|
||||
<VerticalGroup spacing="md">
|
||||
<HorizontalGroup spacing="xs" justify="space-between">
|
||||
<Label description="The public dashboard uses the default time settings of the dashboard">
|
||||
Time Range
|
||||
</Label>
|
||||
<TimeRangeInput value={timeRange} disabled onChange={() => {}} />
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="xs" justify="space-between">
|
||||
<Label description="Configures whether current dashboard can be available publicly">Enabled</Label>
|
||||
<Switch
|
||||
disabled={dashboardHasTemplateVariables(dashboardVariables)}
|
||||
data-testid={selectors.EnableSwitch}
|
||||
value={publicDashboard?.isEnabled}
|
||||
onChange={() => {
|
||||
reportInteraction('grafana_dashboards_public_enable_clicked', {
|
||||
action: publicDashboard?.isEnabled ? 'disable' : 'enable',
|
||||
});
|
||||
|
||||
setPublicDashboardConfig({
|
||||
...publicDashboard,
|
||||
isEnabled: !publicDashboard.isEnabled,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
{publicDashboardPersisted(publicDashboard) && publicDashboard.isEnabled && (
|
||||
<Field label="Link URL" className={styles.publicUrl}>
|
||||
<Input
|
||||
value={generatePublicDashboardUrl(publicDashboard)}
|
||||
readOnly
|
||||
data-testid={selectors.CopyUrlInput}
|
||||
addonAfter={
|
||||
<ClipboardButton
|
||||
data-testid={selectors.CopyUrlButton}
|
||||
variant="primary"
|
||||
icon="copy"
|
||||
getText={() => generatePublicDashboardUrl(publicDashboard)}
|
||||
>
|
||||
Copy
|
||||
</ClipboardButton>
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</FieldSet>
|
||||
{hasWritePermissions ? (
|
||||
props.dashboard.hasUnsavedChanges() && (
|
||||
<Alert
|
||||
title="Please save your dashboard changes before updating the public configuration"
|
||||
severity="warning"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Alert title="You don't have permissions to create or update a public dashboard" severity="warning" />
|
||||
)}
|
||||
<Button
|
||||
disabled={!hasWritePermissions || !acknowledged || props.dashboard.hasUnsavedChanges()}
|
||||
onClick={onSavePublicConfig}
|
||||
data-testid={selectors.SaveConfigButton}
|
||||
>
|
||||
Save sharing configuration
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
checkboxes: css`
|
||||
margin: ${theme.spacing(2, 0)};
|
||||
`,
|
||||
timeRange: css`
|
||||
padding: ${theme.spacing(1, 1)};
|
||||
margin: ${theme.spacing(0, 0, 2, 0)};
|
||||
`,
|
||||
dashboardConfig: css`
|
||||
margin: ${theme.spacing(0, 0, 3, 0)};
|
||||
`,
|
||||
publicUrl: css`
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
`,
|
||||
});
|
@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||
import { Checkbox, FieldSet, HorizontalGroup, LinkButton, VerticalGroup } from '@grafana/ui/src';
|
||||
|
||||
import { Acknowledgements } from './SharePublicDashboardUtils';
|
||||
|
||||
export const AcknowledgeCheckboxes = ({
|
||||
disabled,
|
||||
acknowledgements,
|
||||
onAcknowledge,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
acknowledgements: Acknowledgements;
|
||||
onAcknowledge: (key: string, val: boolean) => void;
|
||||
}) => {
|
||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>Before you click Save, please acknowledge the following information:</p>
|
||||
<FieldSet disabled={disabled}>
|
||||
<VerticalGroup spacing="md">
|
||||
<HorizontalGroup spacing="none">
|
||||
<Checkbox
|
||||
label="Your entire dashboard will be public"
|
||||
value={acknowledgements.public}
|
||||
data-testid={selectors.WillBePublicCheckbox}
|
||||
onChange={(e) => onAcknowledge('public', e.currentTarget.checked)}
|
||||
/>
|
||||
<LinkButton
|
||||
variant="primary"
|
||||
href="https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/"
|
||||
target="_blank"
|
||||
fill="text"
|
||||
icon="info-circle"
|
||||
rel="noopener noreferrer"
|
||||
tooltip="Learn more about public dashboards"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="none">
|
||||
<Checkbox
|
||||
label="Publishing currently only works with a subset of datasources"
|
||||
value={acknowledgements.datasources}
|
||||
data-testid={selectors.LimitedDSCheckbox}
|
||||
onChange={(e) => onAcknowledge('datasources', e.currentTarget.checked)}
|
||||
/>
|
||||
<LinkButton
|
||||
variant="primary"
|
||||
href="https://grafana.com/docs/grafana/latest/datasources/"
|
||||
target="_blank"
|
||||
fill="text"
|
||||
icon="info-circle"
|
||||
rel="noopener noreferrer"
|
||||
tooltip="Learn more about public datasources"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="none">
|
||||
<Checkbox
|
||||
label="Making your dashboard public will cause queries to run each time the dashboard is viewed which may increase costs"
|
||||
value={acknowledgements.usage}
|
||||
data-testid={selectors.CostIncreaseCheckbox}
|
||||
onChange={(e) => onAcknowledge('usage', e.currentTarget.checked)}
|
||||
/>
|
||||
<LinkButton
|
||||
variant="primary"
|
||||
href="https://grafana.com/docs/grafana/latest/enterprise/query-caching/"
|
||||
target="_blank"
|
||||
fill="text"
|
||||
icon="info-circle"
|
||||
rel="noopener noreferrer"
|
||||
tooltip="Learn more about query caching"
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</FieldSet>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,66 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||
import { reportInteraction } from '@grafana/runtime/src';
|
||||
import { FieldSet, Label, Switch, TimeRangeInput, useStyles2, VerticalGroup } from '@grafana/ui/src';
|
||||
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { useIsDesktop } from 'app/features/dashboard/utils/screen';
|
||||
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
|
||||
|
||||
export const Configuration = ({
|
||||
disabled,
|
||||
isPubDashEnabled,
|
||||
hasTemplateVariables,
|
||||
onToggleEnabled,
|
||||
dashboard,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
isPubDashEnabled?: boolean;
|
||||
onToggleEnabled: () => void;
|
||||
hasTemplateVariables: boolean;
|
||||
dashboard: DashboardModel;
|
||||
}) => {
|
||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||
const styles = useStyles2(getStyles);
|
||||
const isDesktop = useIsDesktop();
|
||||
|
||||
const timeRange = getTimeRange(dashboard.getDefaultTime(), dashboard);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="share-modal-info-text">Public dashboard configuration</h4>
|
||||
<FieldSet disabled={disabled} className={styles.dashboardConfig}>
|
||||
<VerticalGroup spacing="md">
|
||||
<Layout orientation={isDesktop ? 0 : 1} spacing="xs" justify="space-between">
|
||||
<Label description="The public dashboard uses the default time settings of the dashboard">Time range</Label>
|
||||
<TimeRangeInput value={timeRange} disabled onChange={() => {}} />
|
||||
</Layout>
|
||||
<Layout orientation={isDesktop ? 0 : 1} spacing="xs" justify="space-between">
|
||||
<Label description="Configures whether current dashboard can be available publicly">Enabled</Label>
|
||||
<Switch
|
||||
disabled={hasTemplateVariables}
|
||||
data-testid={selectors.EnableSwitch}
|
||||
value={isPubDashEnabled}
|
||||
onChange={() => {
|
||||
reportInteraction('grafana_dashboards_public_enable_clicked', {
|
||||
action: isPubDashEnabled ? 'disable' : 'enable',
|
||||
});
|
||||
|
||||
onToggleEnabled();
|
||||
}}
|
||||
/>
|
||||
</Layout>
|
||||
</VerticalGroup>
|
||||
</FieldSet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
dashboardConfig: css`
|
||||
margin: ${theme.spacing(0, 0, 3, 0)};
|
||||
`,
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
export const Description = () => (
|
||||
<>
|
||||
<p>
|
||||
To allow the current dashboard to be published publicly, toggle the switch. For now we do not support template
|
||||
variables or frontend datasources.
|
||||
</p>
|
||||
<p>
|
||||
We'd love your feedback. To share, please comment on this{' '}
|
||||
<a
|
||||
href="https://github.com/grafana/grafana/discussions/49253"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-link"
|
||||
>
|
||||
GitHub discussion
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
);
|
@ -0,0 +1,283 @@
|
||||
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import 'whatwg-fetch';
|
||||
import { BootData } from '@grafana/data/src';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||
import { setEchoSrv } from '@grafana/runtime/src';
|
||||
import config from 'app/core/config';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { Echo } from 'app/core/services/echo/Echo';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { ShareModal } from '../ShareModal';
|
||||
|
||||
const server = setupServer(
|
||||
rest.get('/api/dashboards/uid/:uId/public-config', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
isEnabled: false,
|
||||
uid: undefined,
|
||||
dashboardUid: undefined,
|
||||
accessToken: 'an-access-token',
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getBackendSrv: () => backendSrv,
|
||||
}));
|
||||
|
||||
const renderSharePublicDashboard = async (props: React.ComponentProps<typeof ShareModal>, isEnabled = true) => {
|
||||
const store = configureStore();
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ShareModal {...props} />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
await waitFor(() => screen.getByText('Link'));
|
||||
isEnabled && fireEvent.click(screen.getByText('Public dashboard'));
|
||||
};
|
||||
|
||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||
|
||||
let originalBootData: BootData;
|
||||
let mockDashboard: DashboardModel;
|
||||
let mockPanel: PanelModel;
|
||||
|
||||
beforeAll(() => {
|
||||
setEchoSrv(new Echo());
|
||||
originalBootData = config.bootData;
|
||||
config.appUrl = 'http://dashboards.grafana.com/';
|
||||
config.bootData = {
|
||||
user: {
|
||||
orgId: 1,
|
||||
},
|
||||
navTree: [
|
||||
{
|
||||
text: 'Section name',
|
||||
id: 'section',
|
||||
url: 'section',
|
||||
children: [
|
||||
{ text: 'Child1', id: 'child1', url: 'section/child1' },
|
||||
{ text: 'Child2', id: 'child2', url: 'section/child2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
server.listen({ onUnhandledRequest: 'bypass' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
config.featureToggles.publicDashboards = true;
|
||||
mockDashboard = new DashboardModel({
|
||||
uid: 'mockDashboardUid',
|
||||
});
|
||||
|
||||
mockPanel = new PanelModel({
|
||||
id: 'mockPanelId',
|
||||
});
|
||||
|
||||
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(true);
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
||||
jest.spyOn(contextSrv, 'hasRole').mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.bootData = originalBootData;
|
||||
server.close();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('SharePublic', () => {
|
||||
it('does not render share panel when public dashboards feature is disabled', async () => {
|
||||
config.featureToggles.publicDashboards = false;
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }, false);
|
||||
|
||||
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
||||
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard');
|
||||
});
|
||||
|
||||
it('renders share panel when public dashboards feature is enabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
|
||||
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
|
||||
expect(screen.getByRole('tablist')).toHaveTextContent('Public dashboard');
|
||||
|
||||
fireEvent.click(screen.getByText('Public dashboard'));
|
||||
|
||||
await screen.findByText('Welcome to Grafana public dashboards alpha!');
|
||||
});
|
||||
|
||||
it('renders default relative time in input', async () => {
|
||||
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
|
||||
|
||||
//@ts-ignore
|
||||
mockDashboard.originalTime = { from: 'now-6h', to: 'now' };
|
||||
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
|
||||
await screen.findByText('Welcome to Grafana public dashboards alpha!');
|
||||
expect(screen.getByText('Last 6 hours')).toBeInTheDocument();
|
||||
});
|
||||
it('renders default absolute time in input 2', async () => {
|
||||
mockDashboard.time = { from: '2022-08-30T03:00:00.000Z', to: '2022-09-04T02:59:59.000Z' };
|
||||
//@ts-ignore
|
||||
mockDashboard.originalTime = { from: '2022-08-30T06:00:00.000Z', to: '2022-09-04T06:59:59.000Z' };
|
||||
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
|
||||
await screen.findByText('Welcome to Grafana public dashboards alpha!');
|
||||
expect(screen.getByText('2022-08-30 00:00:00 to 2022-09-04 01:59:59')).toBeInTheDocument();
|
||||
});
|
||||
it('when modal is opened, then loader spinner appears and inputs are disabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
|
||||
expect(await screen.findByTestId('Spinner')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.EnableSwitch)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
||||
});
|
||||
it('when fetch errors happen, then all inputs remain disabled', async () => {
|
||||
server.use(
|
||||
rest.get('/api/dashboards/uid/:uId/public-config', (req, res, ctx) => {
|
||||
return res(ctx.status(500));
|
||||
})
|
||||
);
|
||||
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'), { timeout: 7000 });
|
||||
|
||||
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.EnableSwitch)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
||||
});
|
||||
// test checking if current version of dashboard in state is persisted to db
|
||||
});
|
||||
|
||||
describe('SharePublic - New config setup', () => {
|
||||
it('when modal is opened, then save button is disabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
||||
});
|
||||
it('when fetch is done, then loader spinner is gone, inputs are enabled and save button is disabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeEnabled();
|
||||
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeEnabled();
|
||||
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeEnabled();
|
||||
expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled();
|
||||
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
||||
});
|
||||
it('when checkboxes are filled, then save button remains disabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
|
||||
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
|
||||
fireEvent.click(screen.getByTestId(selectors.CostIncreaseCheckbox));
|
||||
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
|
||||
});
|
||||
it('when checkboxes and switch are filled, then save button is enabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
fireEvent.click(screen.getByTestId(selectors.WillBePublicCheckbox));
|
||||
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
|
||||
fireEvent.click(screen.getByTestId(selectors.CostIncreaseCheckbox));
|
||||
fireEvent.click(screen.getByTestId(selectors.EnableSwitch));
|
||||
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SharePublic - Already persisted', () => {
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get('/api/dashboards/uid/:uId/public-config', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
isEnabled: true,
|
||||
uid: 'a-uid',
|
||||
dashboardUid: req.params.uId,
|
||||
accessToken: 'an-access-token',
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('when modal is opened, then save button is enabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
|
||||
});
|
||||
it('when fetch is done, then loader spinner is gone, inputs are disabled and save button is enabled', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
|
||||
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
|
||||
|
||||
expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled();
|
||||
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
|
||||
});
|
||||
it('when pubdash is enabled, then link url is available', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
expect(screen.getByTestId(selectors.CopyUrlInput)).toBeInTheDocument();
|
||||
});
|
||||
it('when pubdash is disabled in the db, then link url is not available', async () => {
|
||||
server.use(
|
||||
rest.get('/api/dashboards/uid/:uId/public-config', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
isEnabled: false,
|
||||
uid: 'a-uid',
|
||||
dashboardUid: req.params.uId,
|
||||
accessToken: 'an-access-token',
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument();
|
||||
});
|
||||
it('when pubdash is disabled by the user, then link url is not available', async () => {
|
||||
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
|
||||
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
|
||||
|
||||
fireEvent.click(screen.getByTestId(selectors.EnableSwitch));
|
||||
expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,203 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||
import { reportInteraction } from '@grafana/runtime/src';
|
||||
import { Alert, Button, ClipboardButton, Field, HorizontalGroup, Input, useStyles2, Spinner } from '@grafana/ui/src';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { useGetConfigQuery, useSaveConfigMutation } from 'app/features/dashboard/api/publicDashboardApi';
|
||||
import { AcknowledgeCheckboxes } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes';
|
||||
import { Configuration } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/Configuration';
|
||||
import { Description } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/Description';
|
||||
import {
|
||||
Acknowledgements,
|
||||
dashboardHasTemplateVariables,
|
||||
generatePublicDashboardUrl,
|
||||
publicDashboardPersisted,
|
||||
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
||||
import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types';
|
||||
import { isOrgAdmin } from 'app/features/plugins/admin/permissions';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
interface Props extends ShareModalTabProps {}
|
||||
|
||||
export const SharePublicDashboard = (props: Props) => {
|
||||
const dashboardVariables = props.dashboard.getVariables();
|
||||
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const {
|
||||
isLoading: isFetchingLoading,
|
||||
data: publicDashboard,
|
||||
isError: isFetchingError,
|
||||
} = useGetConfigQuery(props.dashboard.uid);
|
||||
|
||||
const [saveConfig, { isLoading: isSaveLoading }] = useSaveConfigMutation();
|
||||
|
||||
const [acknowledgements, setAcknowledgements] = useState<Acknowledgements>({
|
||||
public: false,
|
||||
datasources: false,
|
||||
usage: false,
|
||||
});
|
||||
const [enabledSwitch, setEnabledSwitch] = useState({
|
||||
isEnabled: false,
|
||||
wasTouched: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
reportInteraction('grafana_dashboards_public_share_viewed');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (publicDashboardPersisted(publicDashboard)) {
|
||||
setAcknowledgements({
|
||||
public: true,
|
||||
datasources: true,
|
||||
usage: true,
|
||||
});
|
||||
}
|
||||
|
||||
setEnabledSwitch((prevState) => ({ ...prevState, isEnabled: !!publicDashboard?.isEnabled }));
|
||||
}, [publicDashboard]);
|
||||
|
||||
const isLoading = isFetchingLoading || isSaveLoading;
|
||||
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
|
||||
const acknowledged = acknowledgements.public && acknowledgements.datasources && acknowledgements.usage;
|
||||
const isSaveEnabled = useMemo(
|
||||
() =>
|
||||
!hasWritePermissions ||
|
||||
!acknowledged ||
|
||||
props.dashboard.hasUnsavedChanges() ||
|
||||
isLoading ||
|
||||
isFetchingError ||
|
||||
(!publicDashboardPersisted(publicDashboard) && !enabledSwitch.wasTouched),
|
||||
[hasWritePermissions, acknowledged, props.dashboard, isLoading, isFetchingError, enabledSwitch, publicDashboard]
|
||||
);
|
||||
|
||||
const onSavePublicConfig = () => {
|
||||
reportInteraction('grafana_dashboards_public_create_clicked');
|
||||
|
||||
if (dashboardHasTemplateVariables(dashboardVariables)) {
|
||||
dispatch(
|
||||
notifyApp(createErrorNotification('This dashboard cannot be made public because it has template variables'))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
saveConfig({
|
||||
dashboard: props.dashboard,
|
||||
payload: { ...publicDashboard!, isEnabled: enabledSwitch.isEnabled },
|
||||
});
|
||||
};
|
||||
|
||||
const onAcknowledge = (field: string, checked: boolean) => {
|
||||
setAcknowledgements((prevState) => ({ ...prevState, [field]: checked }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<HorizontalGroup>
|
||||
<p
|
||||
className={css`
|
||||
margin: 0;
|
||||
`}
|
||||
>
|
||||
Welcome to Grafana public dashboards alpha!
|
||||
</p>
|
||||
{isFetchingLoading && <Spinner />}
|
||||
</HorizontalGroup>
|
||||
<div className={styles.content}>
|
||||
{dashboardHasTemplateVariables(dashboardVariables) ? (
|
||||
<Alert
|
||||
severity="warning"
|
||||
title="dashboard cannot be public"
|
||||
data-testid={selectors.TemplateVariablesWarningAlert}
|
||||
>
|
||||
This dashboard cannot be made public because it has template variables
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Description />
|
||||
<hr />
|
||||
<div className={styles.checkboxes}>
|
||||
<AcknowledgeCheckboxes
|
||||
disabled={
|
||||
publicDashboardPersisted(publicDashboard) || !hasWritePermissions || isLoading || isFetchingError
|
||||
}
|
||||
acknowledgements={acknowledgements}
|
||||
onAcknowledge={onAcknowledge}
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<Configuration
|
||||
dashboard={props.dashboard}
|
||||
disabled={!hasWritePermissions || isLoading || isFetchingError}
|
||||
isPubDashEnabled={enabledSwitch.isEnabled}
|
||||
onToggleEnabled={() =>
|
||||
setEnabledSwitch((prevState) => ({ isEnabled: !prevState.isEnabled, wasTouched: true }))
|
||||
}
|
||||
hasTemplateVariables={dashboardHasTemplateVariables(dashboardVariables)}
|
||||
/>
|
||||
{publicDashboardPersisted(publicDashboard) && enabledSwitch.isEnabled && (
|
||||
<Field label="Link URL" className={styles.publicUrl}>
|
||||
<Input
|
||||
disabled={isLoading}
|
||||
value={generatePublicDashboardUrl(publicDashboard!)}
|
||||
readOnly
|
||||
data-testid={selectors.CopyUrlInput}
|
||||
addonAfter={
|
||||
<ClipboardButton
|
||||
data-testid={selectors.CopyUrlButton}
|
||||
variant="primary"
|
||||
icon="copy"
|
||||
getText={() => generatePublicDashboardUrl(publicDashboard!)}
|
||||
>
|
||||
Copy
|
||||
</ClipboardButton>
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{hasWritePermissions ? (
|
||||
props.dashboard.hasUnsavedChanges() && (
|
||||
<Alert
|
||||
title="Please save your dashboard changes before updating the public configuration"
|
||||
severity="warning"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<Alert title="You don't have permissions to create or update a public dashboard" severity="warning" />
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
<Button disabled={isSaveEnabled} onClick={onSavePublicConfig} data-testid={selectors.SaveConfigButton}>
|
||||
Save sharing configuration
|
||||
</Button>
|
||||
{isSaveLoading && <Spinner />}
|
||||
</HorizontalGroup>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
content: css`
|
||||
margin: ${theme.spacing(1, 0, 0, 0)};
|
||||
`,
|
||||
checkboxes: css`
|
||||
margin: ${theme.spacing(2, 0)};
|
||||
`,
|
||||
timeRange: css`
|
||||
padding: ${theme.spacing(1, 1)};
|
||||
margin: ${theme.spacing(0, 0, 2, 0)};
|
||||
`,
|
||||
publicUrl: css`
|
||||
width: 100%;
|
||||
margin-bottom: ${theme.spacing(0, 0, 3, 0)};
|
||||
`,
|
||||
});
|
@ -1,14 +1,11 @@
|
||||
import { updateConfig } from 'app/core/config';
|
||||
import { VariableModel } from 'app/features/variables/types';
|
||||
|
||||
import { updateConfig } from '../../../../core/config';
|
||||
|
||||
import {
|
||||
PublicDashboard,
|
||||
dashboardHasTemplateVariables,
|
||||
generatePublicDashboardUrl,
|
||||
publicDashboardPersisted,
|
||||
getPublicDashboardConfigUrl,
|
||||
savePublicDashboardConfigUrl,
|
||||
} from './SharePublicDashboardUtils';
|
||||
|
||||
describe('dashboardHasTemplateVariables', () => {
|
||||
@ -48,15 +45,3 @@ describe('publicDashboardPersisted', () => {
|
||||
expect(publicDashboardPersisted(pubdash)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPublicDashboardConfigUrl', () => {
|
||||
it('builds the correct url', () => {
|
||||
expect(getPublicDashboardConfigUrl('abc1234')).toEqual('/api/dashboards/uid/abc1234/public-config');
|
||||
});
|
||||
});
|
||||
|
||||
describe('savePublicDashboardConfigUrl', () => {
|
||||
it('builds the correct url', () => {
|
||||
expect(savePublicDashboardConfigUrl('abc1234')).toEqual('/api/dashboards/uid/abc1234/public-config');
|
||||
});
|
||||
});
|
@ -0,0 +1,43 @@
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { VariableModel } from 'app/features/variables/types';
|
||||
import { DashboardDataDTO, DashboardMeta } from 'app/types/dashboard';
|
||||
|
||||
export interface PublicDashboard {
|
||||
accessToken?: string;
|
||||
isEnabled: boolean;
|
||||
uid: string;
|
||||
dashboardUid: string;
|
||||
timeSettings?: object;
|
||||
}
|
||||
|
||||
export interface DashboardResponse {
|
||||
dashboard: DashboardDataDTO;
|
||||
meta: DashboardMeta;
|
||||
}
|
||||
|
||||
export interface Acknowledgements {
|
||||
public: boolean;
|
||||
datasources: boolean;
|
||||
usage: boolean;
|
||||
}
|
||||
|
||||
// Instance methods
|
||||
export const dashboardHasTemplateVariables = (variables: VariableModel[]): boolean => {
|
||||
return variables.length > 0;
|
||||
};
|
||||
|
||||
export const publicDashboardPersisted = (publicDashboard?: PublicDashboard): boolean => {
|
||||
return publicDashboard?.uid !== '' && publicDashboard?.uid !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the public dashboard url. Uses the appUrl from the Grafana boot config, so urls will also be correct
|
||||
* when Grafana is hosted on a subpath.
|
||||
*
|
||||
* All app urls from the Grafana boot config end with a slash.
|
||||
*
|
||||
* @param publicDashboard
|
||||
*/
|
||||
export const generatePublicDashboardUrl = (publicDashboard: PublicDashboard): string => {
|
||||
return `${getConfig().appUrl}public-dashboards/${publicDashboard.accessToken}`;
|
||||
};
|
@ -1,83 +0,0 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
import { VariableModel } from 'app/features/variables/types';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { DashboardDataDTO, DashboardMeta } from 'app/types/dashboard';
|
||||
|
||||
import { DashboardModel } from '../../state';
|
||||
|
||||
export interface PublicDashboard {
|
||||
accessToken?: string;
|
||||
isEnabled: boolean;
|
||||
uid: string;
|
||||
dashboardUid: string;
|
||||
timeSettings?: object;
|
||||
}
|
||||
|
||||
export interface DashboardResponse {
|
||||
dashboard: DashboardDataDTO;
|
||||
meta: DashboardMeta;
|
||||
}
|
||||
|
||||
export const getPublicDashboardConfig = async (
|
||||
dashboardUid: string,
|
||||
setPublicDashboard: React.Dispatch<React.SetStateAction<PublicDashboard>>
|
||||
) => {
|
||||
const pdResp: PublicDashboard = await getBackendSrv().get(getPublicDashboardConfigUrl(dashboardUid));
|
||||
setPublicDashboard(pdResp);
|
||||
};
|
||||
|
||||
export const savePublicDashboardConfig = async (
|
||||
dashboard: DashboardModel,
|
||||
publicDashboardConfig: PublicDashboard,
|
||||
setPublicDashboard: React.Dispatch<React.SetStateAction<PublicDashboard>>
|
||||
) => {
|
||||
const pdResp: PublicDashboard = await getBackendSrv().post(
|
||||
savePublicDashboardConfigUrl(dashboard.uid),
|
||||
publicDashboardConfig
|
||||
);
|
||||
|
||||
// Never allow a user to send the orgId
|
||||
// @ts-ignore
|
||||
delete pdResp.orgId;
|
||||
|
||||
dispatch(notifyApp(createSuccessNotification('Dashboard sharing configuration saved')));
|
||||
setPublicDashboard(pdResp);
|
||||
|
||||
// Update runtime emta flag
|
||||
dashboard.updateMeta({
|
||||
publicDashboardUid: pdResp.uid,
|
||||
publicDashboardEnabled: publicDashboardConfig.isEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
export const getPublicDashboardConfigUrl = (dashboardUid: string) => {
|
||||
return `/api/dashboards/uid/${dashboardUid}/public-config`;
|
||||
};
|
||||
|
||||
export const savePublicDashboardConfigUrl = (dashboardUid: string) => {
|
||||
return `/api/dashboards/uid/${dashboardUid}/public-config`;
|
||||
};
|
||||
|
||||
// Instance methods
|
||||
export const dashboardHasTemplateVariables = (variables: VariableModel[]): boolean => {
|
||||
return variables.length > 0;
|
||||
};
|
||||
|
||||
export const publicDashboardPersisted = (publicDashboard: PublicDashboard): boolean => {
|
||||
return publicDashboard.uid !== '' && publicDashboard.uid !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the public dashboard url. Uses the appUrl from the Grafana boot config, so urls will also be correct
|
||||
* when Grafana is hosted on a subpath.
|
||||
*
|
||||
* All app urls from the Grafana boot config end with a slash.
|
||||
*
|
||||
* @param publicDashboard
|
||||
*/
|
||||
export const generatePublicDashboardUrl = (publicDashboard: PublicDashboard): string => {
|
||||
return `${getConfig().appUrl}public-dashboards/${publicDashboard.accessToken}`;
|
||||
};
|
7
public/app/features/dashboard/utils/screen.ts
Normal file
7
public/app/features/dashboard/utils/screen.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { useWindowSize } from 'react-use';
|
||||
|
||||
export const useIsDesktop = () => {
|
||||
const { width } = useWindowSize();
|
||||
|
||||
return width > 1024;
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import { configureStore as reduxConfigureStore } from '@reduxjs/toolkit';
|
||||
|
||||
import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi';
|
||||
import { StoreState } from 'app/types/store';
|
||||
|
||||
import { buildInitialState } from '../core/reducers/navModel';
|
||||
@ -20,7 +21,8 @@ export function configureStore(initialState?: Partial<StoreState>) {
|
||||
reducer: createRootReducer(),
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({ thunk: true, serializableCheck: false, immutableCheck: false }).concat(
|
||||
alertingApi.middleware
|
||||
alertingApi.middleware,
|
||||
publicDashboardApi.middleware
|
||||
),
|
||||
devTools: process.env.NODE_ENV !== 'production',
|
||||
preloadedState: {
|
||||
|
267
yarn.lock
267
yarn.lock
@ -7682,6 +7682,32 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mswjs/cookies@npm:^0.2.2":
|
||||
version: 0.2.2
|
||||
resolution: "@mswjs/cookies@npm:0.2.2"
|
||||
dependencies:
|
||||
"@types/set-cookie-parser": ^2.4.0
|
||||
set-cookie-parser: ^2.4.6
|
||||
checksum: 23b1ef56d57efcc1b44600076f531a1fb703855af342a31e01bad4adaf0dab51f6d3b5595a95a7988c3f612ba075835f9a06c52833205284d101eb9a51dd72b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mswjs/interceptors@npm:^0.17.5":
|
||||
version: 0.17.5
|
||||
resolution: "@mswjs/interceptors@npm:0.17.5"
|
||||
dependencies:
|
||||
"@open-draft/until": ^1.0.3
|
||||
"@types/debug": ^4.1.7
|
||||
"@xmldom/xmldom": ^0.7.5
|
||||
debug: ^4.3.3
|
||||
headers-polyfill: ^3.1.0
|
||||
outvariant: ^1.2.1
|
||||
strict-event-emitter: ^0.2.4
|
||||
web-encoding: ^1.1.5
|
||||
checksum: 0293ccc56c1c85fb7334cd5902574f7df20c26be74d633c83fde64ffd7620f81e08253fe7985c6b5ad3b64c04ad53c3610e9b9c07621518aabd977343026bb2b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nodelib/fs.scandir@npm:2.1.5":
|
||||
version: 2.1.5
|
||||
resolution: "@nodelib/fs.scandir@npm:2.1.5"
|
||||
@ -8091,6 +8117,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@open-draft/until@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "@open-draft/until@npm:1.0.3"
|
||||
checksum: 323e92ebef0150ed0f8caedc7d219b68cdc50784fa4eba0377eef93533d3f46514eb2400ced83dda8c51bddc3d2c7b8e9cf95e5ec85ab7f62dfc015d174f62f2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/api-metrics@npm:0.25.0":
|
||||
version: 0.25.0
|
||||
resolution: "@opentelemetry/api-metrics@npm:0.25.0"
|
||||
@ -11076,6 +11109,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/cookie@npm:^0.4.1":
|
||||
version: 0.4.1
|
||||
resolution: "@types/cookie@npm:0.4.1"
|
||||
checksum: 3275534ed69a76c68eb1a77d547d75f99fedc80befb75a3d1d03662fb08d697e6f8b1274e12af1a74c6896071b11510631ba891f64d30c78528d0ec45a9c1a18
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/d3-array@npm:*":
|
||||
version: 3.0.2
|
||||
resolution: "@types/d3-array@npm:3.0.2"
|
||||
@ -11417,6 +11457,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/debug@npm:^4.1.7":
|
||||
version: 4.1.7
|
||||
resolution: "@types/debug@npm:4.1.7"
|
||||
dependencies:
|
||||
"@types/ms": "*"
|
||||
checksum: 0a7b89d8ed72526858f0b61c6fd81f477853e8c4415bb97f48b1b5545248d2ae389931680b94b393b993a7cfe893537a200647d93defe6d87159b96812305adc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/deep-freeze@npm:^0.1.1":
|
||||
version: 0.1.2
|
||||
resolution: "@types/deep-freeze@npm:0.1.2"
|
||||
@ -11785,6 +11834,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/js-levenshtein@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "@types/js-levenshtein@npm:1.1.1"
|
||||
checksum: 1d1ff1ee2ad551909e47f3ce19fcf85b64dc5146d3b531c8d26fc775492d36e380b32cf5ef68ff301e812c3b00282f37aac579ebb44498b94baff0ace7509769
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/js-yaml@npm:^4.0.5":
|
||||
version: 4.0.5
|
||||
resolution: "@types/js-yaml@npm:4.0.5"
|
||||
@ -11928,6 +11984,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ms@npm:*":
|
||||
version: 0.7.31
|
||||
resolution: "@types/ms@npm:0.7.31"
|
||||
checksum: daadd354aedde024cce6f5aa873fefe7b71b22cd0e28632a69e8b677aeb48ae8caa1c60e5919bb781df040d116b01cb4316335167a3fc0ef6a63fa3614c0f6da
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node-fetch@npm:^2.5.7":
|
||||
version: 2.6.1
|
||||
resolution: "@types/node-fetch@npm:2.6.1"
|
||||
@ -12485,6 +12548,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/set-cookie-parser@npm:^2.4.0":
|
||||
version: 2.4.2
|
||||
resolution: "@types/set-cookie-parser@npm:2.4.2"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: c31bf04eb9620829dc3c91bced74ac934ad039d20d20893fb5acac0f08769cbd4eef3bf7502a0289c7be59c3e9cfa456147b4e88bff47dd1b9efb4995ba5d5a3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/sinon@npm:10.0.13":
|
||||
version: 10.0.13
|
||||
resolution: "@types/sinon@npm:10.0.13"
|
||||
@ -13625,6 +13697,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@xmldom/xmldom@npm:^0.7.5":
|
||||
version: 0.7.5
|
||||
resolution: "@xmldom/xmldom@npm:0.7.5"
|
||||
checksum: 8d7ec35c1ef6183b4f621df08e01d7e61f244fb964a4719025e65fe6ac06fac418919be64fb40fe5908e69158ef728f2d936daa082db326fe04603012b5f2a84
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@xobotyi/scrollbar-width@npm:^1.9.5":
|
||||
version: 1.9.5
|
||||
resolution: "@xobotyi/scrollbar-width@npm:1.9.5"
|
||||
@ -13646,6 +13725,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@zxing/text-encoding@npm:0.9.0":
|
||||
version: 0.9.0
|
||||
resolution: "@zxing/text-encoding@npm:0.9.0"
|
||||
checksum: c23b12aee7639382e4949961304a1294776afaffa40f579e09ffecd0e5e68cf26ef3edd75009de46da8a536e571448755ca68b3e2ea707d53793c0edb2e2c34a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"JSONStream@npm:^1.0.4":
|
||||
version: 1.3.5
|
||||
resolution: "JSONStream@npm:1.3.5"
|
||||
@ -16077,6 +16163,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "chalk@npm:4.1.1"
|
||||
dependencies:
|
||||
ansi-styles: ^4.1.0
|
||||
supports-color: ^7.1.0
|
||||
checksum: 036e973e665ba1a32c975e291d5f3d549bceeb7b1b983320d4598fb75d70fe20c5db5d62971ec0fe76cdbce83985a00ee42372416abfc3a5584465005a7855ed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^2.0.0, chalk@npm:^2.0.1, chalk@npm:^2.1.0, chalk@npm:^2.3.0, chalk@npm:^2.3.2, chalk@npm:^2.4.1":
|
||||
version: 2.4.2
|
||||
resolution: "chalk@npm:2.4.2"
|
||||
@ -17101,7 +17197,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie@npm:0.4.2":
|
||||
"cookie@npm:0.4.2, cookie@npm:^0.4.2":
|
||||
version: 0.4.2
|
||||
resolution: "cookie@npm:0.4.2"
|
||||
checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b
|
||||
@ -22450,6 +22546,7 @@ __metadata:
|
||||
mousetrap: 1.6.5
|
||||
mousetrap-global-bind: 1.1.0
|
||||
moveable: 0.35.4
|
||||
msw: ^0.47.3
|
||||
mutationobserver-shim: 0.3.7
|
||||
ngtemplate-loader: 2.1.0
|
||||
node-notifier: 10.0.1
|
||||
@ -22555,6 +22652,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"graphql@npm:^15.0.0 || ^16.0.0":
|
||||
version: 16.6.0
|
||||
resolution: "graphql@npm:16.6.0"
|
||||
checksum: bf1d9e3c1938ce3c1a81e909bd3ead1ae4707c577f91cff1ca2eca474bfbc7873d5d7b942e1e9777ff5a8304421dba57a4b76d7a29eb19de8711cb70e3c2415e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"growly@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "growly@npm:1.3.0"
|
||||
@ -22861,6 +22965,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"headers-polyfill@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "headers-polyfill@npm:3.1.0"
|
||||
checksum: a95257065684653b7185efbb9a380c547ea832002991b5adf0d90cd222073da2701be9dc2849d1970ecf15e8c35b383984358566afe6e76ca8ff1dbd7cdce3af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"highlight-words-core@npm:^1.2.0":
|
||||
version: 1.2.2
|
||||
resolution: "highlight-words-core@npm:1.2.2"
|
||||
@ -23689,29 +23800,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"inquirer@npm:^8.2.2":
|
||||
version: 8.2.2
|
||||
resolution: "inquirer@npm:8.2.2"
|
||||
dependencies:
|
||||
ansi-escapes: ^4.2.1
|
||||
chalk: ^4.1.1
|
||||
cli-cursor: ^3.1.0
|
||||
cli-width: ^3.0.0
|
||||
external-editor: ^3.0.3
|
||||
figures: ^3.0.0
|
||||
lodash: ^4.17.21
|
||||
mute-stream: 0.0.8
|
||||
ora: ^5.4.1
|
||||
run-async: ^2.4.0
|
||||
rxjs: ^7.5.5
|
||||
string-width: ^4.1.0
|
||||
strip-ansi: ^6.0.0
|
||||
through: ^2.3.6
|
||||
checksum: 69a2cf32f51af0e94dd66c597fdca42b890ff521b537dbfe1fd532c19a751d54893b7896523691ec30357f6212a80a2417fec7bf34411f369bbf151bdbc95ae9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"inquirer@npm:^8.2.4":
|
||||
"inquirer@npm:^8.2.0, inquirer@npm:^8.2.4":
|
||||
version: 8.2.4
|
||||
resolution: "inquirer@npm:8.2.4"
|
||||
dependencies:
|
||||
@ -23734,6 +23823,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"inquirer@npm:^8.2.2":
|
||||
version: 8.2.2
|
||||
resolution: "inquirer@npm:8.2.2"
|
||||
dependencies:
|
||||
ansi-escapes: ^4.2.1
|
||||
chalk: ^4.1.1
|
||||
cli-cursor: ^3.1.0
|
||||
cli-width: ^3.0.0
|
||||
external-editor: ^3.0.3
|
||||
figures: ^3.0.0
|
||||
lodash: ^4.17.21
|
||||
mute-stream: 0.0.8
|
||||
ora: ^5.4.1
|
||||
run-async: ^2.4.0
|
||||
rxjs: ^7.5.5
|
||||
string-width: ^4.1.0
|
||||
strip-ansi: ^6.0.0
|
||||
through: ^2.3.6
|
||||
checksum: 69a2cf32f51af0e94dd66c597fdca42b890ff521b537dbfe1fd532c19a751d54893b7896523691ec30357f6212a80a2417fec7bf34411f369bbf151bdbc95ae9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"internal-slot@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "internal-slot@npm:1.0.3"
|
||||
@ -24219,6 +24330,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-node-process@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "is-node-process@npm:1.0.1"
|
||||
checksum: 3ddb8a892a00f6eb9c2aea7e7e1426b8683512d9419933d95114f4f64b5455e26601c23a31c0682463890032136dd98a326988a770ab6b4eed54a43ade8bed50
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-number-object@npm:^1.0.4":
|
||||
version: 1.0.6
|
||||
resolution: "is-number-object@npm:1.0.6"
|
||||
@ -25884,6 +26002,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-levenshtein@npm:^1.1.6":
|
||||
version: 1.1.6
|
||||
resolution: "js-levenshtein@npm:1.1.6"
|
||||
checksum: 409f052a7f1141be4058d97da7860e08efd97fc588b7a4c5cfa0548bc04f6d576644dae65ab630266dff685d56fb90d494e03d4d79cb484c287746b4f1bf0694
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-string-escape@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "js-string-escape@npm:1.0.1"
|
||||
@ -28055,6 +28180,41 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"msw@npm:^0.47.3":
|
||||
version: 0.47.3
|
||||
resolution: "msw@npm:0.47.3"
|
||||
dependencies:
|
||||
"@mswjs/cookies": ^0.2.2
|
||||
"@mswjs/interceptors": ^0.17.5
|
||||
"@open-draft/until": ^1.0.3
|
||||
"@types/cookie": ^0.4.1
|
||||
"@types/js-levenshtein": ^1.1.1
|
||||
chalk: 4.1.1
|
||||
chokidar: ^3.4.2
|
||||
cookie: ^0.4.2
|
||||
graphql: ^15.0.0 || ^16.0.0
|
||||
headers-polyfill: ^3.1.0
|
||||
inquirer: ^8.2.0
|
||||
is-node-process: ^1.0.1
|
||||
js-levenshtein: ^1.1.6
|
||||
node-fetch: ^2.6.7
|
||||
outvariant: ^1.3.0
|
||||
path-to-regexp: ^6.2.0
|
||||
statuses: ^2.0.0
|
||||
strict-event-emitter: ^0.2.0
|
||||
type-fest: ^2.19.0
|
||||
yargs: ^17.3.1
|
||||
peerDependencies:
|
||||
typescript: ">= 4.2.x <= 4.8.x"
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
bin:
|
||||
msw: cli/index.js
|
||||
checksum: 1be018c7b2eff982409967cccb5c604e45f65710ee9698bab57fbe794f8426d1a4d33e52b75ef395c6d226948c799241c7c2c7748ec4f5b741e7f25bcbafbd1e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"multicast-dns@npm:^7.2.4":
|
||||
version: 7.2.4
|
||||
resolution: "multicast-dns@npm:7.2.4"
|
||||
@ -29187,6 +29347,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"outvariant@npm:^1.2.1, outvariant@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "outvariant@npm:1.3.0"
|
||||
checksum: ac76ca375c1c642989e1c74f0e9ebac84c05bc9fdc8f28be949c16fae1658e9f1f2fb1133fe3cc1e98afabef78fe4298fe9360b5734baf8e6ad440c182680848
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"overlap-area@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "overlap-area@npm:1.1.0"
|
||||
@ -29731,6 +29898,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-to-regexp@npm:^6.2.0":
|
||||
version: 6.2.1
|
||||
resolution: "path-to-regexp@npm:6.2.1"
|
||||
checksum: f0227af8284ea13300f4293ba111e3635142f976d4197f14d5ad1f124aebd9118783dd2e5f1fe16f7273743cc3dbeddfb7493f237bb27c10fdae07020cc9b698
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-type@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "path-type@npm:3.0.0"
|
||||
@ -34645,6 +34819,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"set-cookie-parser@npm:^2.4.6":
|
||||
version: 2.5.1
|
||||
resolution: "set-cookie-parser@npm:2.5.1"
|
||||
checksum: b99c37f976e68ae6eb7c758bf2bbce1e60bb54e3eccedaa25f2da45b77b9cab58d90674cf9edd7aead6fbeac6308f2eb48713320a47ca120d0e838d0194513b6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"set-harmonic-interval@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "set-harmonic-interval@npm:1.0.1"
|
||||
@ -35567,6 +35748,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"statuses@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "statuses@npm:2.0.1"
|
||||
checksum: 18c7623fdb8f646fb213ca4051be4df7efb3484d4ab662937ca6fbef7ced9b9e12842709872eb3020cc3504b93bde88935c9f6417489627a7786f24f8031cbcb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"store2@npm:^2.12.0":
|
||||
version: 2.13.1
|
||||
resolution: "store2@npm:2.13.1"
|
||||
@ -35649,6 +35837,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strict-event-emitter@npm:^0.2.0, strict-event-emitter@npm:^0.2.4":
|
||||
version: 0.2.4
|
||||
resolution: "strict-event-emitter@npm:0.2.4"
|
||||
dependencies:
|
||||
events: ^3.3.0
|
||||
checksum: fe6af7baf4002910ffd04d16f37c994e7b9f56b4c01c8846a3b394efcea6689a9eba3ebcd5283774476c3a7632aae6b47ef89061b0fbf7f2256b8e07a5cab32d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string-argv@npm:^0.3.1":
|
||||
version: 0.3.1
|
||||
resolution: "string-argv@npm:0.3.1"
|
||||
@ -37274,6 +37471,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-fest@npm:^2.19.0":
|
||||
version: 2.19.0
|
||||
resolution: "type-fest@npm:2.19.0"
|
||||
checksum: a4ef07ece297c9fba78fc1bd6d85dff4472fe043ede98bd4710d2615d15776902b595abf62bd78339ed6278f021235fb28a96361f8be86ed754f778973a0d278
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-is@npm:~1.6.17, type-is@npm:~1.6.18":
|
||||
version: 1.6.18
|
||||
resolution: "type-is@npm:1.6.18"
|
||||
@ -37848,7 +38052,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"util@npm:^0.12.0":
|
||||
"util@npm:^0.12.0, util@npm:^0.12.3":
|
||||
version: 0.12.4
|
||||
resolution: "util@npm:0.12.4"
|
||||
dependencies:
|
||||
@ -38207,6 +38411,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-encoding@npm:^1.1.5":
|
||||
version: 1.1.5
|
||||
resolution: "web-encoding@npm:1.1.5"
|
||||
dependencies:
|
||||
"@zxing/text-encoding": 0.9.0
|
||||
util: ^0.12.3
|
||||
dependenciesMeta:
|
||||
"@zxing/text-encoding":
|
||||
optional: true
|
||||
checksum: 2234a2b122f41006ce07859b3c0bf2e18f46144fda2907d5db0b571b76aa5c26977c646100ad9c00d2f8a4f6f2b848bc02147845d8c447ab365ec4eff376338d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-namespaces@npm:^1.0.0":
|
||||
version: 1.1.4
|
||||
resolution: "web-namespaces@npm:1.1.4"
|
||||
|
Loading…
Reference in New Issue
Block a user