add isPublic to dashboard (#48012)

adds toggle to make a dashboard public

* config struct for public dashboard config
* api endpoints for public dashboard configuration
* ui for toggling public dashboard on and off
* load public dashboard config on share modal

Co-authored-by: Owen Smallwood <owen.smallwood@grafana.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Jeff Levin
2022-05-17 14:11:55 -08:00
committed by GitHub
parent 156e14e296
commit c7f8c2cc73
25 changed files with 607 additions and 108 deletions

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Modal, ModalTabsHeader, TabContent } from '@grafana/ui';
import { config } from 'app/core/config';
import { contextSrv } from 'app/core/core';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard';
@@ -9,6 +10,7 @@ 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';
@@ -52,6 +54,10 @@ function getTabs(props: Props) {
tabs.push(...customDashboardTabs);
}
if (Boolean(config.featureToggles['publicDashboards'])) {
tabs.push({ label: 'Public Dashboard', value: 'share', component: SharePublicDashboard });
}
return tabs;
}

View File

@@ -0,0 +1,76 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import config from 'app/core/config';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { ShareModal } from './ShareModal';
jest.mock('app/core/core', () => {
return {
contextSrv: {
hasPermission: () => true,
},
appEvents: {
subscribe: () => {
return {
unsubscribe: () => {},
};
},
emit: () => {},
},
};
});
describe('SharePublic', () => {
let originalBootData: any;
beforeAll(() => {
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 waitFor(() => screen.getByText('Enabled'));
});
});

View File

@@ -0,0 +1,69 @@
import React, { useState, useEffect } from 'react';
import { Button, Field, Switch } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification, createSuccessNotification } from 'app/core/copy/appNotification';
import { dispatch } from 'app/store/store';
import {
dashboardCanBePublic,
getPublicDashboardConfig,
savePublicDashboardConfig,
PublicDashboardConfig,
} from './SharePublicDashboardUtils';
import { ShareModalTabProps } from './types';
interface Props extends ShareModalTabProps {}
// 1. write test for dashboardCanBePublic
// 2. figure out how to disable the switch
export const SharePublicDashboard = (props: Props) => {
const [publicDashboardConfig, setPublicDashboardConfig] = useState<PublicDashboardConfig>({ isPublic: false });
const dashboardUid = props.dashboard.uid;
useEffect(() => {
getPublicDashboardConfig(dashboardUid)
.then((pdc: PublicDashboardConfig) => {
setPublicDashboardConfig(pdc);
})
.catch(() => {
dispatch(notifyApp(createErrorNotification('Failed to retrieve public dashboard config')));
});
}, [dashboardUid]);
const onSavePublicConfig = () => {
// verify dashboard can be public
if (!dashboardCanBePublic(props.dashboard)) {
dispatch(notifyApp(createErrorNotification('This dashboard cannot be made public')));
return;
}
try {
savePublicDashboardConfig(props.dashboard.uid, publicDashboardConfig);
dispatch(notifyApp(createSuccessNotification('Dashboard sharing configuration saved')));
} catch (err) {
console.error('Error while making dashboard public', err);
dispatch(notifyApp(createErrorNotification('Error making dashboard public')));
}
};
return (
<>
<p className="share-modal-info-text">Public Dashboard Configuration</p>
<Field label="Enabled" description="Configures whether current dashboard can be available publicly">
<Switch
id="share-current-time-range"
disabled={!dashboardCanBePublic(props.dashboard)}
value={publicDashboardConfig?.isPublic}
onChange={() =>
setPublicDashboardConfig((state) => {
return { ...state, isPublic: !state.isPublic };
})
}
/>
</Field>
<Button onClick={onSavePublicConfig}>Save Sharing Configuration</Button>
</>
);
};

View File

@@ -0,0 +1,17 @@
import { DashboardModel } from 'app/features/dashboard/state';
import { dashboardCanBePublic } from './SharePublicDashboardUtils';
describe('dashboardCanBePublic', () => {
it('can be public with no template variables', () => {
//@ts-ignore
const dashboard: DashboardModel = { templating: { list: [] } };
expect(dashboardCanBePublic(dashboard)).toBe(true);
});
it('cannot be public with template variables', () => {
//@ts-ignore
const dashboard: DashboardModel = { templating: { list: [{}] } };
expect(dashboardCanBePublic(dashboard)).toBe(false);
});
});

View File

@@ -0,0 +1,21 @@
import { getBackendSrv } from '@grafana/runtime';
import { DashboardModel } from 'app/features/dashboard/state';
export interface PublicDashboardConfig {
isPublic: boolean;
}
export const dashboardCanBePublic = (dashboard: DashboardModel): boolean => {
return dashboard?.templating?.list.length === 0;
};
export const getPublicDashboardConfig = async (dashboardUid: string) => {
const url = `/api/dashboards/uid/${dashboardUid}/public-config`;
return getBackendSrv().get(url);
};
export const savePublicDashboardConfig = async (dashboardUid: string, conf: PublicDashboardConfig) => {
const payload = { isPublic: conf.isPublic };
const url = `/api/dashboards/uid/${dashboardUid}/public-config`;
return getBackendSrv().post(url, payload);
};