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,101 +1,141 @@
import React from 'react';
import { appEvents } from 'app/core/core';
import { Icon } from '@grafana/ui';
import { css } from '@emotion/css';
import { GrafanaThemeV2 } from '@grafana/data';
import { Modal, useStyles2 } from '@grafana/ui';
export class HelpModal extends React.PureComponent {
static tabIndex = 0;
static shortcuts = {
Global: [
{ keys: ['g', 'h'], description: 'Go to Home Dashboard' },
{ keys: ['g', 'p'], description: 'Go to Profile' },
{ keys: ['s', 'o'], description: 'Open search' },
{ keys: ['esc'], description: 'Exit edit/setting views' },
],
Dashboard: [
{ keys: ['mod+s'], description: 'Save dashboard' },
{ keys: ['d', 'r'], description: 'Refresh all panels' },
{ keys: ['d', 's'], description: 'Dashboard settings' },
{ keys: ['d', 'v'], description: 'Toggle in-active / view mode' },
{ keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)' },
{ keys: ['d', 'E'], description: 'Expand all rows' },
{ keys: ['d', 'C'], description: 'Collapse all rows' },
{ keys: ['d', 'a'], description: 'Toggle auto fit panels (experimental feature)' },
{ keys: ['mod+o'], description: 'Toggle shared graph crosshair' },
{ keys: ['d', 'l'], description: 'Toggle all panel legends' },
],
'Focused Panel': [
{ keys: ['e'], description: 'Toggle panel edit view' },
{ keys: ['v'], description: 'Toggle panel fullscreen view' },
{ keys: ['p', 's'], description: 'Open Panel Share Modal' },
{ keys: ['p', 'd'], description: 'Duplicate Panel' },
{ keys: ['p', 'r'], description: 'Remove Panel' },
{ keys: ['p', 'l'], description: 'Toggle panel legend' },
],
'Time Range': [
{ keys: ['t', 'z'], description: 'Zoom out time range' },
{
keys: ['t', '←'],
description: 'Move time range back',
},
{
keys: ['t', '→'],
description: 'Move time range forward',
},
],
};
const shortcuts = {
Global: [
{ keys: ['g', 'h'], description: 'Go to Home Dashboard' },
{ keys: ['g', 'p'], description: 'Go to Profile' },
{ keys: ['s', 'o'], description: 'Open search' },
{ keys: ['esc'], description: 'Exit edit/setting views' },
],
Dashboard: [
{ keys: ['mod+s'], description: 'Save dashboard' },
{ keys: ['d', 'r'], description: 'Refresh all panels' },
{ keys: ['d', 's'], description: 'Dashboard settings' },
{ keys: ['d', 'v'], description: 'Toggle in-active / view mode' },
{ keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)' },
{ keys: ['d', 'E'], description: 'Expand all rows' },
{ keys: ['d', 'C'], description: 'Collapse all rows' },
{ keys: ['d', 'a'], description: 'Toggle auto fit panels (experimental feature)' },
{ keys: ['mod+o'], description: 'Toggle shared graph crosshair' },
{ keys: ['d', 'l'], description: 'Toggle all panel legends' },
],
'Focused Panel': [
{ keys: ['e'], description: 'Toggle panel edit view' },
{ keys: ['v'], description: 'Toggle panel fullscreen view' },
{ keys: ['p', 's'], description: 'Open Panel Share Modal' },
{ keys: ['p', 'd'], description: 'Duplicate Panel' },
{ keys: ['p', 'r'], description: 'Remove Panel' },
{ keys: ['p', 'l'], description: 'Toggle panel legend' },
],
'Time Range': [
{ keys: ['t', 'z'], description: 'Zoom out time range' },
{
keys: ['t', '←'],
description: 'Move time range back',
},
{
keys: ['t', '→'],
description: 'Move time range forward',
},
],
};
dismiss() {
appEvents.emit('hide-modal');
}
render() {
return (
<div className="modal-body">
<div className="modal-header">
<h2 className="modal-header-title">
<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 className="modal-content help-modal">
<p className="small" style={{ position: 'absolute', top: '13px', right: '44px' }}>
<span className="shortcut-table-key">mod</span> =
<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>
<tr>
<th className="shortcut-table-category-header" colSpan={2}>
{category}
</th>
</tr>
{shortcuts.map((shortcut, j) => (
<tr key={`${i}-${j}`}>
<td className="shortcut-table-keys">
{shortcut.keys.map((key, k) => (
<span className="shortcut-table-key" key={`${i}-${j}-${k}`}>
{key}
</span>
))}
</td>
<td className="shortcut-table-description">{shortcut.description}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
<div className="clearfix" />
</div>
</div>
);
}
export interface HelpModalProps {
onDismiss: () => void;
}
export const HelpModal = ({ onDismiss }: HelpModalProps): JSX.Element => {
const styles = useStyles2(getStyles);
return (
<Modal title="Shortcuts" isOpen onDismiss={onDismiss} onClickBackdrop={onDismiss}>
<div className={styles.titleDescription}>
<span className={styles.shortcutTableKey}>mod</span> =<span> CTRL on windows or linux and CMD key on Mac</span>
</div>
<div className={styles.categories}>
{Object.entries(shortcuts).map(([category, shortcuts], i) => (
<div className={styles.shortcutCategory} key={i}>
<table className={styles.shortcutTable}>
<tbody>
<tr>
<th className={styles.shortcutTableCategoryHeader} colSpan={2}>
{category}
</th>
</tr>
{shortcuts.map((shortcut, j) => (
<tr key={`${i}-${j}`}>
<td className={styles.shortcutTableKeys}>
{shortcut.keys.map((key, k) => (
<span className={styles.shortcutTableKey} key={`${i}-${j}-${k}`}>
{key}
</span>
))}
</td>
<td className={styles.shortcutTableDescription}>{shortcut.description}</td>
</tr>
))}
</tbody>
</table>
</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 BottomNavLinks from './BottomNavLinks';
import appEvents from '../../app_events';
import { ShowModalEvent } from '../../../types/events';
import { ShowModalReactEvent } from '../../../types/events';
import { HelpModal } from '../help/HelpModal';
jest.mock('../../app_events', () => ({
publish: jest.fn(),
@ -94,11 +95,7 @@ describe('Functions', () => {
const instance = wrapper.instance() as BottomNavLinks;
instance.onOpenShortcuts();
expect(appEvents.publish).toHaveBeenCalledWith(
new ShowModalEvent({
templateHtml: '<help-modal></help-modal>',
})
);
expect(appEvents.publish).toHaveBeenCalledWith(new ShowModalReactEvent({ component: HelpModal }));
});
});
});

View File

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

View File

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

View File

@ -15,7 +15,7 @@ import {
ShowModalReactEvent,
} from '../../types/events';
import { ConfirmModal, ConfirmModalProps } from '@grafana/ui';
import { textUtil } from '@grafana/data';
import { deprecationWarning, textUtil } from '@grafana/data';
export class UtilSrv {
modalScope: any;
@ -55,13 +55,21 @@ export class UtilSrv {
this.reactModalRoot.removeChild(this.reactModalNode);
};
/**
* @deprecated use showModalReact instead that has this capability built in
*/
hideModal() {
deprecationWarning('UtilSrv', 'hideModal', 'showModalReact');
if (this.modalScope && this.modalScope.dismiss) {
this.modalScope.dismiss();
}
}
/**
* @deprecated use showModalReact instead
*/
showModal(options: any) {
deprecationWarning('UtilSrv', 'showModal', 'showModalReact');
if (this.modalScope && 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 { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { locationService } from '@grafana/runtime';
import { ShowModalEvent } from '../../types/events';
import { ShowModalReactEvent } from '../../types/events';
import { AlertHowToModal } from './AlertHowToModal';
jest.mock('../../core/app_events', () => ({
publish: jest.fn(),
@ -92,13 +93,7 @@ describe('Functions', () => {
instance.onOpenHowTo();
expect(appEvents.publish).toHaveBeenCalledWith(
new ShowModalEvent({
src: 'public/app/features/alerting/partials/alert_howto.html',
modalClass: 'confirm-modal',
model: {},
})
);
expect(appEvents.publish).toHaveBeenCalledWith(new ShowModalReactEvent({ component: AlertHowToModal }));
});
});

View File

@ -15,7 +15,8 @@ import { setSearchQuery } from './state/reducers';
import { Button, LinkButton, Select, VerticalGroup } from '@grafana/ui';
import { AlertDefinitionItem } from './components/AlertDefinitionItem';
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) {
return {
@ -73,13 +74,7 @@ export class AlertRuleListUnconnected extends PureComponent<Props> {
};
onOpenHowTo = () => {
appEvents.publish(
new ShowModalEvent({
src: 'public/app/features/alerting/partials/alert_howto.html',
modalClass: 'confirm-modal',
model: {},
})
);
appEvents.publish(new ShowModalReactEvent({ component: AlertHowToModal }));
};
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 props: Props = {
onDismiss: jest.fn(),
apiKey: 'api key test',
rootPath: 'test/path',
};

View File

@ -1,47 +1,44 @@
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 {
onDismiss: () => void;
apiKey: string;
rootPath: string;
}
export const ApiKeysAddedModal = (props: Props) => {
export function ApiKeysAddedModal({ onDismiss, apiKey, rootPath }: Props): JSX.Element {
const styles = useStyles2(getStyles);
return (
<div className="modal-body">
<div className="modal-header">
<h2 className="modal-header-title">
<Icon name="key-skeleton-alt" size="lg" />
<span className="p-l-1">API Key Created</span>
</h2>
<Modal title="API Key Created" onDismiss={onDismiss} onClickBackdrop={onDismiss} isOpen>
<Field label="Key">
<span className={styles.label}>{apiKey}</span>
</Field>
<a className="modal-header-close" ng-click="dismiss();">
<Icon name="times" />
</a>
</div>
<Alert severity="info" title="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.
</Alert>
<div className="modal-content">
<div className="gf-form-group">
<div className="gf-form">
<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>
</div>
</div>
</div>
<p className="text-muted">You can authenticate a request using the Authorization HTTP header, example:</p>
<pre className={styles.small}>
curl -H &quot;Authorization: Bearer {apiKey}&quot; {rootPath}/api/dashboards/home
</pre>
</Modal>
);
};
}
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 ReactDOMServer from 'react-dom/server';
import { connect, ConnectedProps } from 'react-redux';
import { hot } from 'react-hot-loader';
// Utils
@ -8,7 +7,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
import { getApiKeys, getApiKeysCount } from './state/selectors';
import { addApiKey, deleteApiKey, loadApiKeys } from './state/actions';
import Page from 'app/core/components/Page/Page';
import ApiKeysAddedModal from './ApiKeysAddedModal';
import { ApiKeysAddedModal } from './ApiKeysAddedModal';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
@ -20,7 +19,7 @@ import { ApiKeysForm } from './ApiKeysForm';
import { ApiKeysActionBar } from './ApiKeysActionBar';
import { ApiKeysTable } from './ApiKeysTable';
import { ApiKeysController } from './ApiKeysController';
import { ShowModalEvent } from 'app/types/events';
import { ShowModalReactEvent } from 'app/types/events';
const { Switch } = LegacyForms;
@ -82,11 +81,14 @@ export class ApiKeysPageUnconnected extends PureComponent<Props, State> {
onAddApiKey = (newApiKey: NewApiKey) => {
const openModal = (apiKey: string) => {
const rootPath = window.location.origin + config.appSubUrl;
const modalTemplate = ReactDOMServer.renderToString(<ApiKeysAddedModal apiKey={apiKey} rootPath={rootPath} />);
appEvents.publish(
new ShowModalEvent({
templateHtml: modalTemplate,
new ShowModalReactEvent({
props: {
apiKey,
rootPath,
},
component: ApiKeysAddedModal,
})
);
};

View File

@ -1,79 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div
className="modal-body"
<Modal
isOpen={true}
onClickBackdrop={[MockFunction]}
onDismiss={[MockFunction]}
title="API Key Created"
>
<div
className="modal-header"
<Field
label="Key"
>
<h2
className="modal-header-title"
<span
className="css-ypoxjz"
>
<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"
api key test
</span>
</Field>
<Alert
severity="info"
title="You will only be able to view this key here once!"
>
<div
className="gf-form-group"
>
<div
className="gf-form"
>
<span
className="gf-form-label"
>
Key
</span>
<span
className="gf-form-label"
>
api key test
</span>
</div>
</div>
<div
className="grafana-info-box"
style={
Object {
"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 "Authorization: Bearer
api key test
"
test/path
/api/dashboards/home
</pre>
</div>
</div>
</div>
It is not stored in this form, so be sure to copy it now.
</Alert>
<p
className="text-muted"
>
You can authenticate a request using the Authorization HTTP header, example:
</p>
<pre
className="css-omfua5"
>
curl -H "Authorization: Bearer
api key test
"
test/path
/api/dashboards/home
</pre>
</Modal>
`;

View File

@ -4,7 +4,8 @@ import { Button, Field, Modal, Switch } from '@grafana/ui';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal';
import { appEvents } from 'app/core/core';
import { ShowModalEvent } from 'app/types/events';
import { ShowModalReactEvent } from 'app/types/events';
import { ViewJsonModal } from './ViewJsonModal';
interface Props {
dashboard: DashboardModel;
@ -70,15 +71,12 @@ export class ShareExport extends PureComponent<Props, State> {
};
openJsonModal = (clone: object) => {
const model = {
object: clone,
enableCopy: true,
};
appEvents.publish(
new ShowModalEvent({
src: 'public/app/partials/edit_json.html',
model,
new ShowModalReactEvent({
props: {
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,
} from '@grafana/data';
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 { getPluginSettings } from './PluginSettingsCache';
@ -31,8 +31,9 @@ import { config } from 'app/core/config';
import { contextSrv } from '../../core/services/context_srv';
import { css } from '@emotion/css';
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 { UpdatePluginModal } from './UpdatePluginModal';
interface Props extends GrafanaRouteComponentProps<{ pluginId: string }, UrlQueryMap> {}
@ -142,10 +143,14 @@ class PluginPage extends PureComponent<Props, State> {
}
showUpdateInfo = () => {
const { id, name } = this.state.plugin!.meta;
appEvents.publish(
new ShowModalEvent({
src: 'public/app/features/plugins/partials/update_instructions.html',
model: this.state.plugin!.meta,
new ShowModalReactEvent({
props: {
id,
name,
},
component: UpdatePluginModal,
})
);
};
@ -162,9 +167,12 @@ class PluginPage extends PureComponent<Props, State> {
{meta.hasUpdate && (
<div>
<Tooltip content={meta.latestVersion!} theme="info" placement="top">
<a href="#" onClick={this.showUpdateInfo}>
<LinkButton fill="text" onClick={this.showUpdateInfo}>
Update Available!
</a>
</LinkButton>
{/*<a href="#" onClick={this.showUpdateInfo}>*/}
{/* Update Available!*/}
{/*</a>*/}
</Tooltip>
</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';
}
/**
* @deprecated use ShowModalReactEvent instead that has this capability built in
*/
export class ShowModalEvent extends BusEventWithPayload<ShowModalPayload> {
static type = 'show-modal';
}
@ -184,6 +187,9 @@ export class ShowModalReactEvent extends BusEventWithPayload<ShowModalReactPaylo
static type = 'show-react-modal';
}
/**
* @deprecated use ShowModalReactEvent instead that has this capability built in
*/
export class HideModalEvent extends BusEventBase {
static type = 'hide-modal';
}

View File

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

View File

@ -55,14 +55,3 @@
.pluginlist-emphasis {
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;
}