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:
juanicabanas 2022-09-22 09:35:04 -03:00 committed by GitHub
parent 2f14575dd3
commit c7419de2e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1016 additions and 560 deletions

View File

@ -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": [

View File

@ -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')

View File

@ -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",

View File

@ -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 = {};

View 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;

View File

@ -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';

View File

@ -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
});

View File

@ -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&apos;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;
`,
});

View File

@ -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>
</>
);
};

View File

@ -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)};
`,
});

View File

@ -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&apos;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>
</>
);

View File

@ -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();
});
});

View File

@ -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)};
`,
});

View File

@ -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');
});
});

View File

@ -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}`;
};

View File

@ -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}`;
};

View File

@ -0,0 +1,7 @@
import { useWindowSize } from 'react-use';
export const useIsDesktop = () => {
const { width } = useWindowSize();
return width > 1024;
};

View File

@ -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
View File

@ -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"