Chore: Migrates remaining Angular modals to React (#33476)

* HelpModal: Migrates to new style

* Alerting: Migrates how to do alerting modal to React

* ApiKeysModal: migrates to new theme

* Dashboard: View dasboard json modal migrated to React and new theme

* PluginPage: migrates update plugin modal to react and new theme

* Chore: deprecates events and functions

* Simplify help modal

* Updated json modal to use Modal.ButtonRow

* Tweak to api key design

* Tests: updates snapshot

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Hugo Häggmark 2021-04-28 15:22:28 +02:00 committed by GitHub
parent 86a57d17d2
commit 22ac0fc3cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 371 additions and 385 deletions

View File

@ -1,10 +1,9 @@
import React from 'react'; import React from 'react';
import { appEvents } from 'app/core/core'; import { css } from '@emotion/css';
import { Icon } from '@grafana/ui'; import { GrafanaThemeV2 } from '@grafana/data';
import { Modal, useStyles2 } from '@grafana/ui';
export class HelpModal extends React.PureComponent { const shortcuts = {
static tabIndex = 0;
static shortcuts = {
Global: [ Global: [
{ keys: ['g', 'h'], description: 'Go to Home Dashboard' }, { keys: ['g', 'h'], description: 'Go to Home Dashboard' },
{ keys: ['g', 'p'], description: 'Go to Profile' }, { keys: ['g', 'p'], description: 'Go to Profile' },
@ -44,58 +43,99 @@ export class HelpModal extends React.PureComponent {
], ],
}; };
dismiss() { export interface HelpModalProps {
appEvents.emit('hide-modal'); onDismiss: () => void;
} }
render() { export const HelpModal = ({ onDismiss }: HelpModalProps): JSX.Element => {
const styles = useStyles2(getStyles);
return ( return (
<div className="modal-body"> <Modal title="Shortcuts" isOpen onDismiss={onDismiss} onClickBackdrop={onDismiss}>
<div className="modal-header"> <div className={styles.titleDescription}>
<h2 className="modal-header-title"> <span className={styles.shortcutTableKey}>mod</span> =<span> CTRL on windows or linux and CMD key on Mac</span>
<Icon name="keyboard" size="lg" />
<span className="p-l-1">Shortcuts</span>
</h2>
<a className="modal-header-close" onClick={this.dismiss}>
<Icon name="times" style={{ margin: '3px 0 0 0' }} />
</a>
</div> </div>
<div className={styles.categories}>
<div className="modal-content help-modal"> {Object.entries(shortcuts).map(([category, shortcuts], i) => (
<p className="small" style={{ position: 'absolute', top: '13px', right: '44px' }}> <div className={styles.shortcutCategory} key={i}>
<span className="shortcut-table-key">mod</span> = <table className={styles.shortcutTable}>
<span className="muted"> CTRL on windows or linux and CMD key on Mac</span>
</p>
{Object.entries(HelpModal.shortcuts).map(([category, shortcuts], i) => (
<div className="shortcut-category" key={i}>
<table className="shortcut-table">
<tbody> <tbody>
<tr> <tr>
<th className="shortcut-table-category-header" colSpan={2}> <th className={styles.shortcutTableCategoryHeader} colSpan={2}>
{category} {category}
</th> </th>
</tr> </tr>
{shortcuts.map((shortcut, j) => ( {shortcuts.map((shortcut, j) => (
<tr key={`${i}-${j}`}> <tr key={`${i}-${j}`}>
<td className="shortcut-table-keys"> <td className={styles.shortcutTableKeys}>
{shortcut.keys.map((key, k) => ( {shortcut.keys.map((key, k) => (
<span className="shortcut-table-key" key={`${i}-${j}-${k}`}> <span className={styles.shortcutTableKey} key={`${i}-${j}-${k}`}>
{key} {key}
</span> </span>
))} ))}
</td> </td>
<td className="shortcut-table-description">{shortcut.description}</td> <td className={styles.shortcutTableDescription}>{shortcut.description}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
))} ))}
<div className="clearfix" />
</div>
</div> </div>
</Modal>
); );
} };
function getStyles(theme: GrafanaThemeV2) {
return {
titleDescription: css`
font-size: ${theme.typography.bodySmall.fontSize};
font-weight: ${theme.typography.bodySmall.fontWeight};
color: ${theme.colors.text.disabled};
padding-bottom: ${theme.spacing(2)};
`,
categories: css`
font-size: ${theme.typography.bodySmall.fontSize};
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: flex-start;
`,
shortcutCategory: css`
width: 50%;
font-size: ${theme.typography.bodySmall.fontSize};
`,
shortcutTable: css`
margin-bottom: ${theme.spacing(2)};
`,
shortcutTableCategoryHeader: css`
font-weight: normal;
font-size: ${theme.typography.h6.fontSize};
text-align: left;
`,
shortcutTableDescription: css`
text-align: left;
color: ${theme.colors.text.disabled};
width: 99%;
padding: ${theme.spacing(1, 2)};
`,
shortcutTableKeys: css`
white-space: nowrap;
width: 1%;
text-align: right;
color: ${theme.colors.text.primary};
`,
shortcutTableKey: css`
display: inline-block;
text-align: center;
margin-right: ${theme.spacing(0.5)};
padding: 3px 5px;
font: 11px Consolas, 'Liberation Mono', Menlo, Courier, monospace;
line-height: 10px;
vertical-align: middle;
border: solid 1px ${theme.colors.border.medium};
border-radius: ${theme.shape.borderRadius(3)};
color: ${theme.colors.text.primary};
background-color: ${theme.colors.background.secondary};
`,
};
} }

View File

@ -2,7 +2,8 @@ import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import BottomNavLinks from './BottomNavLinks'; import BottomNavLinks from './BottomNavLinks';
import appEvents from '../../app_events'; import appEvents from '../../app_events';
import { ShowModalEvent } from '../../../types/events'; import { ShowModalReactEvent } from '../../../types/events';
import { HelpModal } from '../help/HelpModal';
jest.mock('../../app_events', () => ({ jest.mock('../../app_events', () => ({
publish: jest.fn(), publish: jest.fn(),
@ -94,11 +95,7 @@ describe('Functions', () => {
const instance = wrapper.instance() as BottomNavLinks; const instance = wrapper.instance() as BottomNavLinks;
instance.onOpenShortcuts(); instance.onOpenShortcuts();
expect(appEvents.publish).toHaveBeenCalledWith( expect(appEvents.publish).toHaveBeenCalledWith(new ShowModalReactEvent({ component: HelpModal }));
new ShowModalEvent({
templateHtml: '<help-modal></help-modal>',
})
);
}); });
}); });
}); });

View File

@ -6,7 +6,8 @@ import { NavModelItem } from '@grafana/data';
import { Icon, IconName, Link } from '@grafana/ui'; import { Icon, IconName, Link } from '@grafana/ui';
import { OrgSwitcher } from '../OrgSwitcher'; import { OrgSwitcher } from '../OrgSwitcher';
import { getFooterLinks } from '../Footer/Footer'; import { getFooterLinks } from '../Footer/Footer';
import { ShowModalEvent } from '../../../types/events'; import { ShowModalReactEvent } from '../../../types/events';
import { HelpModal } from '../help/HelpModal';
export interface Props { export interface Props {
link: NavModelItem; link: NavModelItem;
@ -23,11 +24,7 @@ export default class BottomNavLinks extends PureComponent<Props, State> {
}; };
onOpenShortcuts = () => { onOpenShortcuts = () => {
appEvents.publish( appEvents.publish(new ShowModalReactEvent({ component: HelpModal }));
new ShowModalEvent({
templateHtml: '<help-modal></help-modal>',
})
);
}; };
toggleSwitcherModal = () => { toggleSwitcherModal = () => {

View File

@ -22,6 +22,7 @@ import { getDatasourceSrv } from '../../features/plugins/datasource_srv';
import { getTimeSrv } from '../../features/dashboard/services/TimeSrv'; import { getTimeSrv } from '../../features/dashboard/services/TimeSrv';
import { toggleTheme } from './toggleTheme'; import { toggleTheme } from './toggleTheme';
import { withFocusedPanel } from './withFocusedPanelId'; import { withFocusedPanel } from './withFocusedPanelId';
import { HelpModal } from '../components/help/HelpModal';
export class KeybindingSrv { export class KeybindingSrv {
modalOpen = false; modalOpen = false;
@ -97,7 +98,7 @@ export class KeybindingSrv {
} }
private showHelpModal() { private showHelpModal() {
appEvents.publish(new ShowModalEvent({ templateHtml: '<help-modal></help-modal>' })); appEvents.publish(new ShowModalReactEvent({ component: HelpModal }));
} }
private exit() { private exit() {

View File

@ -15,7 +15,7 @@ import {
ShowModalReactEvent, ShowModalReactEvent,
} from '../../types/events'; } from '../../types/events';
import { ConfirmModal, ConfirmModalProps } from '@grafana/ui'; import { ConfirmModal, ConfirmModalProps } from '@grafana/ui';
import { textUtil } from '@grafana/data'; import { deprecationWarning, textUtil } from '@grafana/data';
export class UtilSrv { export class UtilSrv {
modalScope: any; modalScope: any;
@ -55,13 +55,21 @@ export class UtilSrv {
this.reactModalRoot.removeChild(this.reactModalNode); this.reactModalRoot.removeChild(this.reactModalNode);
}; };
/**
* @deprecated use showModalReact instead that has this capability built in
*/
hideModal() { hideModal() {
deprecationWarning('UtilSrv', 'hideModal', 'showModalReact');
if (this.modalScope && this.modalScope.dismiss) { if (this.modalScope && this.modalScope.dismiss) {
this.modalScope.dismiss(); this.modalScope.dismiss();
} }
} }
/**
* @deprecated use showModalReact instead
*/
showModal(options: any) { showModal(options: any) {
deprecationWarning('UtilSrv', 'showModal', 'showModalReact');
if (this.modalScope && this.modalScope.dismiss) { if (this.modalScope && this.modalScope.dismiss) {
this.modalScope.dismiss(); this.modalScope.dismiss();
} }

View File

@ -0,0 +1,21 @@
import { Modal, VerticalGroup } from '@grafana/ui';
import React from 'react';
export interface AlertHowToModalProps {
onDismiss: () => void;
}
export function AlertHowToModal({ onDismiss }: AlertHowToModalProps): JSX.Element {
return (
<Modal title="Adding an Alert" isOpen onDismiss={onDismiss} onClickBackdrop={onDismiss}>
<VerticalGroup spacing="sm">
<img src="public/img/alert_howto_new.png" alt="link to how to alert image" />
<p>
Alerts are added and configured in the Alert tab of any dashboard graph panel, letting you build and visualize
an alert using existing queries.
</p>
<p>Remember to save the dashboard to persist your alert rule changes.</p>
</VerticalGroup>
</Modal>
);
}

View File

@ -8,7 +8,8 @@ import { setSearchQuery } from './state/reducers';
import { mockToolkitActionCreator } from 'test/core/redux/mocks'; import { mockToolkitActionCreator } from 'test/core/redux/mocks';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { ShowModalEvent } from '../../types/events'; import { ShowModalReactEvent } from '../../types/events';
import { AlertHowToModal } from './AlertHowToModal';
jest.mock('../../core/app_events', () => ({ jest.mock('../../core/app_events', () => ({
publish: jest.fn(), publish: jest.fn(),
@ -92,13 +93,7 @@ describe('Functions', () => {
instance.onOpenHowTo(); instance.onOpenHowTo();
expect(appEvents.publish).toHaveBeenCalledWith( expect(appEvents.publish).toHaveBeenCalledWith(new ShowModalReactEvent({ component: AlertHowToModal }));
new ShowModalEvent({
src: 'public/app/features/alerting/partials/alert_howto.html',
modalClass: 'confirm-modal',
model: {},
})
);
}); });
}); });

View File

@ -15,7 +15,8 @@ import { setSearchQuery } from './state/reducers';
import { Button, LinkButton, Select, VerticalGroup } from '@grafana/ui'; import { Button, LinkButton, Select, VerticalGroup } from '@grafana/ui';
import { AlertDefinitionItem } from './components/AlertDefinitionItem'; import { AlertDefinitionItem } from './components/AlertDefinitionItem';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { ShowModalEvent } from '../../types/events'; import { ShowModalReactEvent } from '../../types/events';
import { AlertHowToModal } from './AlertHowToModal';
function mapStateToProps(state: StoreState) { function mapStateToProps(state: StoreState) {
return { return {
@ -73,13 +74,7 @@ export class AlertRuleListUnconnected extends PureComponent<Props> {
}; };
onOpenHowTo = () => { onOpenHowTo = () => {
appEvents.publish( appEvents.publish(new ShowModalReactEvent({ component: AlertHowToModal }));
new ShowModalEvent({
src: 'public/app/features/alerting/partials/alert_howto.html',
modalClass: 'confirm-modal',
model: {},
})
);
}; };
onSearchQueryChange = (value: string) => { onSearchQueryChange = (value: string) => {

View File

@ -1,29 +0,0 @@
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
<icon name="'info-circle'"></icon>
<span class="p-l-1">Adding an Alert</span>
</h2>
<a class="modal-header-close" ng-click="dismiss();">
<icon name="'times'"></icon>
</a>
</div>
<div class="modal-content">
<div class="text-center">
<img src="public/img/alert_howto_new.png"></img>
</div>
<p class="p-a-2 text-center offset-lg-1 col-lg-10">
Alerts are added and configured in the Alert tab of any dashboard
graph panel, letting you build and visualize an alert using existing queries.
<br> <br>
Remember to save the dashboard to persist your alert rule changes.
<br>
<br>
</p>
</div>
</div>

View File

@ -4,6 +4,7 @@ import { ApiKeysAddedModal, Props } from './ApiKeysAddedModal';
const setup = (propOverrides?: object) => { const setup = (propOverrides?: object) => {
const props: Props = { const props: Props = {
onDismiss: jest.fn(),
apiKey: 'api key test', apiKey: 'api key test',
rootPath: 'test/path', rootPath: 'test/path',
}; };

View File

@ -1,47 +1,44 @@
import React from 'react'; import React from 'react';
import { Icon } from '@grafana/ui'; import { css } from '@emotion/css';
import { Alert, Field, Modal, useStyles2 } from '@grafana/ui';
import { GrafanaThemeV2 } from '@grafana/data';
export interface Props { export interface Props {
onDismiss: () => void;
apiKey: string; apiKey: string;
rootPath: string; rootPath: string;
} }
export const ApiKeysAddedModal = (props: Props) => { export function ApiKeysAddedModal({ onDismiss, apiKey, rootPath }: Props): JSX.Element {
const styles = useStyles2(getStyles);
return ( return (
<div className="modal-body"> <Modal title="API Key Created" onDismiss={onDismiss} onClickBackdrop={onDismiss} isOpen>
<div className="modal-header"> <Field label="Key">
<h2 className="modal-header-title"> <span className={styles.label}>{apiKey}</span>
<Icon name="key-skeleton-alt" size="lg" /> </Field>
<span className="p-l-1">API Key Created</span>
</h2>
<a className="modal-header-close" ng-click="dismiss();"> <Alert severity="info" title="You will only be able to view this key here once!">
<Icon name="times" /> It is not stored in this form, so be sure to copy it now.
</a> </Alert>
</div>
<div className="modal-content"> <p className="text-muted">You can authenticate a request using the Authorization HTTP header, example:</p>
<div className="gf-form-group"> <pre className={styles.small}>
<div className="gf-form"> curl -H &quot;Authorization: Bearer {apiKey}&quot; {rootPath}/api/dashboards/home
<span className="gf-form-label">Key</span>
<span className="gf-form-label">{props.apiKey}</span>
</div>
</div>
<div className="grafana-info-box" style={{ border: 0 }}>
You will only be able to view this key here once! It is not stored in this form, so be sure to copy it now.
<br />
<br />
You can authenticate a request using the Authorization HTTP header, example:
<br />
<br />
<pre className="small">
curl -H &quot;Authorization: Bearer {props.apiKey}&quot; {props.rootPath}/api/dashboards/home
</pre> </pre>
</div> </Modal>
</div>
</div>
); );
}; }
export default ApiKeysAddedModal; function getStyles(theme: GrafanaThemeV2) {
return {
label: css`
padding: ${theme.spacing(1)};
background-color: ${theme.colors.background.secondary};
border-radius: ${theme.shape.borderRadius()};
`,
small: css`
font-size: ${theme.typography.bodySmall.fontSize};
font-weight: ${theme.typography.bodySmall.fontWeight};
`,
};
}

View File

@ -1,5 +1,4 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import ReactDOMServer from 'react-dom/server';
import { connect, ConnectedProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
// Utils // Utils
@ -8,7 +7,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
import { getApiKeys, getApiKeysCount } from './state/selectors'; import { getApiKeys, getApiKeysCount } from './state/selectors';
import { addApiKey, deleteApiKey, loadApiKeys } from './state/actions'; import { addApiKey, deleteApiKey, loadApiKeys } from './state/actions';
import Page from 'app/core/components/Page/Page'; import Page from 'app/core/components/Page/Page';
import ApiKeysAddedModal from './ApiKeysAddedModal'; import { ApiKeysAddedModal } from './ApiKeysAddedModal';
import config from 'app/core/config'; import config from 'app/core/config';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
@ -20,7 +19,7 @@ import { ApiKeysForm } from './ApiKeysForm';
import { ApiKeysActionBar } from './ApiKeysActionBar'; import { ApiKeysActionBar } from './ApiKeysActionBar';
import { ApiKeysTable } from './ApiKeysTable'; import { ApiKeysTable } from './ApiKeysTable';
import { ApiKeysController } from './ApiKeysController'; import { ApiKeysController } from './ApiKeysController';
import { ShowModalEvent } from 'app/types/events'; import { ShowModalReactEvent } from 'app/types/events';
const { Switch } = LegacyForms; const { Switch } = LegacyForms;
@ -82,11 +81,14 @@ export class ApiKeysPageUnconnected extends PureComponent<Props, State> {
onAddApiKey = (newApiKey: NewApiKey) => { onAddApiKey = (newApiKey: NewApiKey) => {
const openModal = (apiKey: string) => { const openModal = (apiKey: string) => {
const rootPath = window.location.origin + config.appSubUrl; const rootPath = window.location.origin + config.appSubUrl;
const modalTemplate = ReactDOMServer.renderToString(<ApiKeysAddedModal apiKey={apiKey} rootPath={rootPath} />);
appEvents.publish( appEvents.publish(
new ShowModalEvent({ new ShowModalReactEvent({
templateHtml: modalTemplate, props: {
apiKey,
rootPath,
},
component: ApiKeysAddedModal,
}) })
); );
}; };

View File

@ -1,71 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = ` exports[`Render should render component 1`] = `
<div <Modal
className="modal-body" isOpen={true}
onClickBackdrop={[MockFunction]}
onDismiss={[MockFunction]}
title="API Key Created"
> >
<div <Field
className="modal-header" label="Key"
>
<h2
className="modal-header-title"
>
<Icon
name="key-skeleton-alt"
size="lg"
/>
<span
className="p-l-1"
>
API Key Created
</span>
</h2>
<a
className="modal-header-close"
ng-click="dismiss();"
>
<Icon
name="times"
/>
</a>
</div>
<div
className="modal-content"
>
<div
className="gf-form-group"
>
<div
className="gf-form"
> >
<span <span
className="gf-form-label" className="css-ypoxjz"
>
Key
</span>
<span
className="gf-form-label"
> >
api key test api key test
</span> </span>
</div> </Field>
</div> <Alert
<div severity="info"
className="grafana-info-box" title="You will only be able to view this key here once!"
style={ >
Object { It is not stored in this form, so be sure to copy it now.
"border": 0, </Alert>
} <p
} className="text-muted"
> >
You will only be able to view this key here once! It is not stored in this form, so be sure to copy it now.
<br />
<br />
You can authenticate a request using the Authorization HTTP header, example: You can authenticate a request using the Authorization HTTP header, example:
<br /> </p>
<br />
<pre <pre
className="small" className="css-omfua5"
> >
curl -H "Authorization: Bearer curl -H "Authorization: Bearer
api key test api key test
@ -73,7 +36,5 @@ exports[`Render should render component 1`] = `
test/path test/path
/api/dashboards/home /api/dashboards/home
</pre> </pre>
</div> </Modal>
</div>
</div>
`; `;

View File

@ -4,7 +4,8 @@ import { Button, Field, Modal, Switch } from '@grafana/ui';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal'; import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal';
import { appEvents } from 'app/core/core'; import { appEvents } from 'app/core/core';
import { ShowModalEvent } from 'app/types/events'; import { ShowModalReactEvent } from 'app/types/events';
import { ViewJsonModal } from './ViewJsonModal';
interface Props { interface Props {
dashboard: DashboardModel; dashboard: DashboardModel;
@ -70,15 +71,12 @@ export class ShareExport extends PureComponent<Props, State> {
}; };
openJsonModal = (clone: object) => { openJsonModal = (clone: object) => {
const model = {
object: clone,
enableCopy: true,
};
appEvents.publish( appEvents.publish(
new ShowModalEvent({ new ShowModalReactEvent({
src: 'public/app/partials/edit_json.html', props: {
model, json: JSON.stringify(clone, null, 2),
},
component: ViewJsonModal,
}) })
); );

View File

@ -0,0 +1,31 @@
import React, { useCallback } from 'react';
import { ClipboardButton, CodeEditor, Modal } from '@grafana/ui';
import AutoSizer from 'react-virtualized-auto-sizer';
import { notifyApp } from '../../../../core/actions';
import { dispatch } from '../../../../store/store';
import { createSuccessNotification } from '../../../../core/copy/appNotification';
export interface ViewJsonModalProps {
json: string;
onDismiss: () => void;
}
export function ViewJsonModal({ json, onDismiss }: ViewJsonModalProps): JSX.Element {
const getClipboardText = useCallback(() => json, [json]);
const onClipboardCopy = () => {
dispatch(notifyApp(createSuccessNotification('Content copied to clipboard')));
};
return (
<Modal title="JSON" onDismiss={onDismiss} onClickBackdrop={onDismiss} isOpen>
<AutoSizer disableHeight>
{({ width }) => <CodeEditor value={json} language="json" showMiniMap={false} height="500px" width={width} />}
</AutoSizer>
<Modal.ButtonRow>
<ClipboardButton getText={getClipboardText} onClipboardCopy={onClipboardCopy}>
Copy to Clipboard
</ClipboardButton>
</Modal.ButtonRow>
</Modal>
);
}

View File

@ -17,7 +17,7 @@ import {
UrlQueryMap, UrlQueryMap,
} from '@grafana/data'; } from '@grafana/data';
import { AppNotificationSeverity } from 'app/types'; import { AppNotificationSeverity } from 'app/types';
import { Alert, InfoBox, Tooltip, PluginSignatureBadge } from '@grafana/ui'; import { Alert, InfoBox, LinkButton, PluginSignatureBadge, Tooltip } from '@grafana/ui';
import Page from 'app/core/components/Page/Page'; import Page from 'app/core/components/Page/Page';
import { getPluginSettings } from './PluginSettingsCache'; import { getPluginSettings } from './PluginSettingsCache';
@ -31,8 +31,9 @@ import { config } from 'app/core/config';
import { contextSrv } from '../../core/services/context_srv'; import { contextSrv } from '../../core/services/context_srv';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { ShowModalEvent } from 'app/types/events'; import { ShowModalReactEvent } from 'app/types/events';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { UpdatePluginModal } from './UpdatePluginModal';
interface Props extends GrafanaRouteComponentProps<{ pluginId: string }, UrlQueryMap> {} interface Props extends GrafanaRouteComponentProps<{ pluginId: string }, UrlQueryMap> {}
@ -142,10 +143,14 @@ class PluginPage extends PureComponent<Props, State> {
} }
showUpdateInfo = () => { showUpdateInfo = () => {
const { id, name } = this.state.plugin!.meta;
appEvents.publish( appEvents.publish(
new ShowModalEvent({ new ShowModalReactEvent({
src: 'public/app/features/plugins/partials/update_instructions.html', props: {
model: this.state.plugin!.meta, id,
name,
},
component: UpdatePluginModal,
}) })
); );
}; };
@ -162,9 +167,12 @@ class PluginPage extends PureComponent<Props, State> {
{meta.hasUpdate && ( {meta.hasUpdate && (
<div> <div>
<Tooltip content={meta.latestVersion!} theme="info" placement="top"> <Tooltip content={meta.latestVersion!} theme="info" placement="top">
<a href="#" onClick={this.showUpdateInfo}> <LinkButton fill="text" onClick={this.showUpdateInfo}>
Update Available! Update Available!
</a> </LinkButton>
{/*<a href="#" onClick={this.showUpdateInfo}>*/}
{/* Update Available!*/}
{/*</a>*/}
</Tooltip> </Tooltip>
</div> </div>
)} )}

View File

@ -0,0 +1,58 @@
import React from 'react';
import { Modal, useStyles2, VerticalGroup } from '@grafana/ui';
import { GrafanaThemeV2 } from '@grafana/data';
import { css } from '@emotion/css';
export interface UpdatePluginModalProps {
onDismiss: () => void;
id: string;
name: string;
}
export function UpdatePluginModal({ onDismiss, id, name }: UpdatePluginModalProps): JSX.Element {
const styles = useStyles2(getStyles);
return (
<Modal title="Update Plugin" onDismiss={onDismiss} onClickBackdrop={onDismiss} isOpen>
<VerticalGroup spacing="md">
<VerticalGroup spacing="sm">
<p>Type the following on the command line to update {name}.</p>
<pre>
<code>grafana-cli plugins update {id}</code>
</pre>
<span className={styles.small}>
Check out {name} on <a href={`https://grafana.com/plugins/${id}`}>Grafana.com</a> for README and changelog.
If you do not have access to the command line, ask your Grafana administator.
</span>
</VerticalGroup>
<p className={styles.weak}>
<img className={styles.logo} src="public/img/grafana_icon.svg" alt="grafana logo" />
<strong>Pro tip</strong>: To update all plugins at once, type{' '}
<code className={styles.codeSmall}>grafana-cli plugins update-all</code> on the command line.
</p>
</VerticalGroup>
</Modal>
);
}
function getStyles(theme: GrafanaThemeV2) {
return {
small: css`
font-size: ${theme.typography.bodySmall.fontSize};
font-weight: ${theme.typography.bodySmall.fontWeight};
`,
weak: css`
color: ${theme.colors.text.disabled};
font-size: ${theme.typography.bodySmall.fontSize};
`,
logo: css`
vertical-align: sub;
margin-right: ${theme.spacing(0.3)};
width: ${theme.spacing(2)};
`,
codeSmall: css`
white-space: nowrap;
margin: 0 ${theme.spacing(0.25)};
padding: ${theme.spacing(0.25)};
`,
};
}

View File

@ -1,21 +0,0 @@
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
<icon name="'cloud-download'" size="'lg'"></icon>
<span class="p-l-1">Update Plugin</span>
</h2>
<a class="modal-header-close" ng-click="dismiss();">
<icon name="'times'"></icon>
</a>
</div>
<div class="modal-content">
<div class="gf-form-group">
<p>Type the following on the command line to update {{model.name}}.</p>
<pre><code>grafana-cli plugins update {{model.id}}</code></pre>
<span class="small">Check out {{model.name}} on <a href="https://grafana.com/plugins/{{model.id}}">Grafana.com</a> for README and changelog. If you do not have access to the command line, ask your Grafana administator.</span>
</div>
<p class="pluginlist-none-installed"><img class="pluginlist-inline-logo" src="public/img/grafana_icon.svg"><strong>Pro tip</strong>: To update all plugins at once, type <code class="code--small">grafana-cli plugins update-all</code> on the command line.</div>
</div>
</div>

View File

@ -1,24 +0,0 @@
<div ng-controller="JsonEditorCtrl">
<div class="tabbed-view-header">
<h2 class="tabbed-view-title">
JSON
</h2>
<button class="tabbed-view-close-btn" ng-click="dismiss()">
<icon name="'times'"></icon>
</button>
</div>
<div class="tabbed-view-body">
<div class="gf-form">
<code-editor content="json" data-mode="json" data-max-lines="20"></code-editor>
</div>
<div class="gf-form-button-row">
<button type="button" class="btn btn-primary" ng-show="canUpdate" ng-click="update(); dismiss();">Update</button>
<button class="btn btn-secondary" ng-if="canCopy" clipboard-button="getContentForClipboard()">
Copy to Clipboard
</button>
</div>
</div>
</div>

View File

@ -172,6 +172,9 @@ export class RemovePanelEvent extends BusEventWithPayload<number> {
static type = 'remove-panel'; static type = 'remove-panel';
} }
/**
* @deprecated use ShowModalReactEvent instead that has this capability built in
*/
export class ShowModalEvent extends BusEventWithPayload<ShowModalPayload> { export class ShowModalEvent extends BusEventWithPayload<ShowModalPayload> {
static type = 'show-modal'; static type = 'show-modal';
} }
@ -184,6 +187,9 @@ export class ShowModalReactEvent extends BusEventWithPayload<ShowModalReactPaylo
static type = 'show-react-modal'; static type = 'show-react-modal';
} }
/**
* @deprecated use ShowModalReactEvent instead that has this capability built in
*/
export class HideModalEvent extends BusEventBase { export class HideModalEvent extends BusEventBase {
static type = 'hide-modal'; static type = 'hide-modal';
} }

View File

@ -68,7 +68,6 @@
@import 'components/dropdown'; @import 'components/dropdown';
@import 'components/footer'; @import 'components/footer';
@import 'components/infobox'; @import 'components/infobox';
@import 'components/shortcuts';
@import 'components/drop'; @import 'components/drop';
@import 'components/query_editor'; @import 'components/query_editor';
@import 'components/tabbed_view'; @import 'components/tabbed_view';

View File

@ -55,14 +55,3 @@
.pluginlist-emphasis { .pluginlist-emphasis {
font-weight: $font-weight-semi-bold; font-weight: $font-weight-semi-bold;
} }
.pluginlist-none-installed {
color: $text-color-weak;
font-size: $font-size-sm;
}
.pluginlist-inline-logo {
vertical-align: sub;
margin-right: $spacer / 3;
width: 16px;
}

View File

@ -1,44 +0,0 @@
.shortcut-category {
float: left;
font-size: $font-size-sm;
width: 50%;
}
.shortcut-table {
margin-bottom: $spacer;
.shortcut-table-category-header {
font-weight: normal;
font-size: $font-size-h6;
text-align: left;
}
.shortcut-table-description {
text-align: left;
color: $text-muted;
width: 99%;
padding: $space-sm $space-md;
}
.shortcut-table-keys {
white-space: nowrap;
width: 1%;
text-align: right;
color: $text-color;
}
}
.shortcut-table-key {
display: inline-block;
text-align: center;
margin-right: $space-xs;
padding: 3px 5px;
font: 11px Consolas, 'Liberation Mono', Menlo, Courier, monospace;
line-height: 10px;
vertical-align: middle;
background-color: $btn-inverse-bg;
border: solid 1px $btn-inverse-bg-hl;
border-radius: 3px;
color: $btn-inverse-text-color;
box-shadow: inset 0 -1px 0 $btn-inverse-bg-hl;
}