From a093fbb51af7793488cbbe718e3967efdf06af67 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Tue, 3 Dec 2019 21:47:20 +0000 Subject: [PATCH] Migration: Migrate org switcher to react (#19607) * Migration: Migrate org switcher to react * Improve modal overflow behavior * Updated modal backdrop * Renamed type * Modal: Refactoring and reducing duplication --- packages/grafana-data/src/types/index.ts | 1 + packages/grafana-data/src/types/orgs.ts | 11 +++ .../grafana-ui/src/components/Modal/Modal.tsx | 46 +++++++--- public/app/core/components/OrgSwitcher.tsx | 81 ++++++++++++++++++ public/app/core/components/org_switcher.ts | 84 ------------------- .../components/sidemenu/BottomNavLinks.tsx | 26 ++++-- .../BottomNavLinks.test.tsx.snap | 16 ++++ public/app/core/core.ts | 2 - public/app/core/specs/OrgSwitcher.test.tsx | 51 +++++++++++ public/app/core/specs/org_switcher.test.ts | 48 ----------- .../components/Inspector/PanelInspector.tsx | 11 +-- public/sass/components/_modals.scss | 4 +- 12 files changed, 216 insertions(+), 165 deletions(-) create mode 100644 packages/grafana-data/src/types/orgs.ts create mode 100644 public/app/core/components/OrgSwitcher.tsx delete mode 100644 public/app/core/components/org_switcher.ts create mode 100644 public/app/core/specs/OrgSwitcher.test.tsx delete mode 100644 public/app/core/specs/org_switcher.test.ts diff --git a/packages/grafana-data/src/types/index.ts b/packages/grafana-data/src/types/index.ts index e9f5bd435d6..8169e8094dd 100644 --- a/packages/grafana-data/src/types/index.ts +++ b/packages/grafana-data/src/types/index.ts @@ -18,6 +18,7 @@ export * from './datasource'; export * from './panel'; export * from './plugin'; export * from './theme'; +export * from './orgs'; import * as AppEvents from './appEvents'; import { AppEvent } from './appEvents'; diff --git a/packages/grafana-data/src/types/orgs.ts b/packages/grafana-data/src/types/orgs.ts new file mode 100644 index 00000000000..e4e83a03777 --- /dev/null +++ b/packages/grafana-data/src/types/orgs.ts @@ -0,0 +1,11 @@ +export interface UserOrgDTO { + orgId: number; + name: string; + role: OrgRole; +} + +export enum OrgRole { + Admin = 'Admin', + Editor = 'Editor', + Viewer = 'Viewer', +} diff --git a/packages/grafana-ui/src/components/Modal/Modal.tsx b/packages/grafana-ui/src/components/Modal/Modal.tsx index e5febd9535e..bc99a92e8fe 100644 --- a/packages/grafana-ui/src/components/Modal/Modal.tsx +++ b/packages/grafana-ui/src/components/Modal/Modal.tsx @@ -8,13 +8,12 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({ modal: css` position: fixed; z-index: ${theme.zIndex.modal}; - width: 100%; background: ${theme.colors.pageBg}; box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); background-clip: padding-box; outline: none; - - max-width: 750px; + width: 750px; + max-width: 100%; left: 0; right: 0; margin-left: auto; @@ -28,20 +27,25 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({ bottom: 0; left: 0; z-index: ${theme.zIndex.modalBackdrop}; - background-color: ${theme.colors.bodyBg}; + background-color: ${theme.colors.blueFaint}; opacity: 0.8; backdrop-filter: blur(4px); `, modalHeader: css` background: ${theme.background.pageHeader}; box-shadow: ${theme.shadow.pageHeader}; - border-bottom: 1px soliod ${theme.colors.pageHeaderBorder}; + border-bottom: 1px solid ${theme.colors.pageHeaderBorder}; display: flex; `, modalHeaderTitle: css` font-size: ${theme.typography.heading.h3}; - padding-top: calc(${theme.spacing.d} * 0.75); - margin: 0 calc(${theme.spacing.d} * 3) 0 calc(${theme.spacing.d} * 1.5); + padding-top: ${theme.spacing.sm}; + margin: 0 ${theme.spacing.md}; + `, + modalHeaderIcon: css` + position: relative; + top: 2px; + padding-right: ${theme.spacing.md}; `, modalHeaderClose: css` margin-left: auto; @@ -49,10 +53,14 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({ `, modalContent: css` padding: calc(${theme.spacing.d} * 2); + overflow: auto; + width: 100%; + max-height: calc(90vh - ${theme.spacing.d} * 2); `, })); interface Props { + icon?: string; title: string | JSX.Element; theme: GrafanaTheme; @@ -74,6 +82,18 @@ export class UnthemedModal extends React.PureComponent { this.onDismiss(); }; + renderDefaultHeader() { + const { title, icon, theme } = this.props; + const styles = getStyles(theme); + + return ( +

+ {icon && } + {title} +

+ ); + } + render() { const { title, isOpen = false, theme } = this.props; const styles = getStyles(theme); @@ -84,16 +104,16 @@ export class UnthemedModal extends React.PureComponent { return ( -
-
- {typeof title === 'string' ?

{title}

: <>{title}} - + -
+
); } diff --git a/public/app/core/components/OrgSwitcher.tsx b/public/app/core/components/OrgSwitcher.tsx new file mode 100644 index 00000000000..ef08336848f --- /dev/null +++ b/public/app/core/components/OrgSwitcher.tsx @@ -0,0 +1,81 @@ +import React from 'react'; + +import { getBackendSrv } from '@grafana/runtime'; +import { UserOrgDTO } from '@grafana/data'; +import { Modal, Button } from '@grafana/ui'; + +import { contextSrv } from 'app/core/services/context_srv'; +import config from 'app/core/config'; + +interface Props { + onDismiss: () => void; + isOpen: boolean; +} + +interface State { + orgs: UserOrgDTO[]; +} + +export class OrgSwitcher extends React.PureComponent { + state: State = { + orgs: [], + }; + + componentDidMount() { + this.getUserOrgs(); + } + + getUserOrgs = async () => { + const orgs: UserOrgDTO[] = await getBackendSrv().get('/api/user/orgs'); + this.setState({ + orgs: orgs.sort((a, b) => a.orgId - b.orgId), + }); + }; + + setCurrentOrg = async (org: UserOrgDTO) => { + await getBackendSrv().post(`/api/user/using/${org.orgId}`); + this.setWindowLocation(`${config.appSubUrl}${config.appSubUrl.endsWith('/') ? '' : '/'}?orgId=${org.orgId}`); + }; + + setWindowLocation(href: string) { + window.location.href = href; + } + + render() { + const { onDismiss, isOpen } = this.props; + const { orgs } = this.state; + + const currentOrgId = contextSrv.user.orgId; + + return ( + + + + + + + + + + {orgs.map(org => ( + + + + + + ))} + +
NameRole +
{org.name}{org.role} + {org.orgId === currentOrgId ? ( + + ) : ( + + )} +
+
+ ); + } +} diff --git a/public/app/core/components/org_switcher.ts b/public/app/core/components/org_switcher.ts deleted file mode 100644 index 0e25482e190..00000000000 --- a/public/app/core/components/org_switcher.ts +++ /dev/null @@ -1,84 +0,0 @@ -import coreModule from 'app/core/core_module'; -import { contextSrv } from 'app/core/services/context_srv'; -import config from 'app/core/config'; -import { BackendSrv } from '../services/backend_srv'; - -const template = ` -`; - -export class OrgSwitchCtrl { - orgs: any[]; - currentOrgId: any; - - /** @ngInject */ - constructor(private backendSrv: BackendSrv) { - this.currentOrgId = contextSrv.user.orgId; - this.getUserOrgs(); - } - - getUserOrgs() { - this.backendSrv.get('/api/user/orgs').then((orgs: any) => { - this.orgs = orgs; - }); - } - - setUsingOrg(org: any) { - return this.backendSrv.post('/api/user/using/' + org.orgId).then(() => { - this.setWindowLocation(config.appSubUrl + (config.appSubUrl.endsWith('/') ? '' : '/') + '?orgId=' + org.orgId); - }); - } - - setWindowLocation(href: string) { - window.location.href = href; - } -} - -export function orgSwitcher() { - return { - restrict: 'E', - template: template, - controller: OrgSwitchCtrl, - bindToController: true, - controllerAs: 'ctrl', - scope: { dismiss: '&' }, - }; -} - -coreModule.directive('orgSwitcher', orgSwitcher); diff --git a/public/app/core/components/sidemenu/BottomNavLinks.tsx b/public/app/core/components/sidemenu/BottomNavLinks.tsx index f86f353e3a6..1a3790e06ef 100644 --- a/public/app/core/components/sidemenu/BottomNavLinks.tsx +++ b/public/app/core/components/sidemenu/BottomNavLinks.tsx @@ -3,13 +3,22 @@ import appEvents from '../../app_events'; import { User } from '../../services/context_srv'; import { NavModelItem } from '@grafana/data'; import { CoreEvents } from 'app/types'; +import { OrgSwitcher } from '../OrgSwitcher'; export interface Props { link: NavModelItem; user: User; } -class BottomNavLinks extends PureComponent { +interface State { + showSwitcherModal: boolean; +} + +class BottomNavLinks extends PureComponent { + state: State = { + showSwitcherModal: false, + }; + itemClicked = (event: React.SyntheticEvent, child: NavModelItem) => { if (child.url === '/shortcuts') { event.preventDefault(); @@ -19,14 +28,16 @@ class BottomNavLinks extends PureComponent { } }; - switchOrg = () => { - appEvents.emit(CoreEvents.showModal, { - templateHtml: '', - }); + toggleSwitcherModal = () => { + this.setState(prevState => ({ + showSwitcherModal: !prevState.showSwitcherModal, + })); }; render() { const { link, user } = this.props; + const { showSwitcherModal } = this.state; + return (
@@ -43,7 +54,7 @@ class BottomNavLinks extends PureComponent { )} {link.showOrgSwitcher && (
  • - +
  • )} + + + {link.children && link.children.map((child, index) => { if (!child.hideFromMenu) { diff --git a/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap index 8b15bb0ae36..3c81e496f7e 100644 --- a/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap +++ b/public/app/core/components/sidemenu/__snapshots__/BottomNavLinks.test.tsx.snap @@ -15,6 +15,10 @@ exports[`Render should render children 1`] = ` className="dropdown-menu dropdown-menu--sidemenu" role="menu" > +
  • @@ -62,6 +66,10 @@ exports[`Render should render component 1`] = ` className="dropdown-menu dropdown-menu--sidemenu" role="menu" > +
  • @@ -118,6 +126,10 @@ exports[`Render should render organization switcher 1`] = `
  • +
  • @@ -153,6 +165,10 @@ exports[`Render should render subtitle 1`] = ` subtitle
  • +
  • diff --git a/public/app/core/core.ts b/public/app/core/core.ts index de9db0b4e2d..bcad92e0660 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -39,7 +39,6 @@ import { contextSrv } from './services/context_srv'; import { KeybindingSrv } from './services/keybindingSrv'; import { NavModelSrv } from './nav_model_srv'; import { geminiScrollbar } from './components/scroll/scroll'; -import { orgSwitcher } from './components/org_switcher'; import { profiler } from './profiler'; import { registerAngularDirectives } from './angular_wrappers'; import { updateLegendValues } from './time_series2'; @@ -72,7 +71,6 @@ export { NavModelSrv, NavModel, geminiScrollbar, - orgSwitcher, manageDashboardsDirective, TimeSeries, updateLegendValues, diff --git a/public/app/core/specs/OrgSwitcher.test.tsx b/public/app/core/specs/OrgSwitcher.test.tsx new file mode 100644 index 00000000000..3f86a45ffea --- /dev/null +++ b/public/app/core/specs/OrgSwitcher.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +// @ts-ignore +import { getBackendSrv } from '@grafana/runtime/src/services/backendSrv'; +import { OrgSwitcher } from '../components/OrgSwitcher'; +import { shallow } from 'enzyme'; +import { OrgRole } from '@grafana/data'; + +const getMock = jest.fn(() => Promise.resolve([])); +const postMock = jest.fn(); + +jest.mock('@grafana/runtime/src/services/backendSrv', () => ({ + getBackendSrv: () => ({ + get: getMock, + post: postMock, + }), +})); + +jest.mock('app/core/services/context_srv', () => ({ + contextSrv: { + user: { orgId: 1 }, + }, +})); + +jest.mock('app/core/config', () => { + return { + appSubUrl: '/subUrl', + }; +}); + +let wrapper; +let orgSwitcher: OrgSwitcher; + +describe('OrgSwitcher', () => { + describe('when switching org', () => { + beforeEach(async () => { + wrapper = shallow( {}} isOpen={true} />); + orgSwitcher = wrapper.instance() as OrgSwitcher; + orgSwitcher.setWindowLocation = jest.fn(); + wrapper.update(); + await orgSwitcher.setCurrentOrg({ name: 'mock org', orgId: 2, role: OrgRole.Viewer }); + }); + + it('should switch orgId in call to backend', () => { + expect(postMock).toBeCalledWith('/api/user/using/2'); + }); + + it('should switch orgId in url and redirect to home page', () => { + expect(orgSwitcher.setWindowLocation).toBeCalledWith('/subUrl/?orgId=2'); + }); + }); +}); diff --git a/public/app/core/specs/org_switcher.test.ts b/public/app/core/specs/org_switcher.test.ts deleted file mode 100644 index 4f23738860a..00000000000 --- a/public/app/core/specs/org_switcher.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { OrgSwitchCtrl } from '../components/org_switcher'; -// @ts-ignore -import q from 'q'; - -jest.mock('app/core/services/context_srv', () => ({ - contextSrv: { - user: { orgId: 1 }, - }, -})); - -jest.mock('app/core/config', () => { - return { - appSubUrl: '/subUrl', - }; -}); - -describe('OrgSwitcher', () => { - describe('when switching org', () => { - let expectedHref: string; - let expectedUsingUrl: string; - - beforeEach(() => { - const backendSrvStub: any = { - get: (url: string) => { - return q.resolve([]); - }, - post: (url: string) => { - expectedUsingUrl = url; - return q.resolve({}); - }, - }; - - const orgSwitcherCtrl = new OrgSwitchCtrl(backendSrvStub); - - orgSwitcherCtrl.setWindowLocation = href => (expectedHref = href); - - return orgSwitcherCtrl.setUsingOrg({ orgId: 2 }); - }); - - it('should switch orgId in call to backend', () => { - expect(expectedUsingUrl).toBe('/api/user/using/2'); - }); - - it('should switch orgId in url and redirect to home page', () => { - expect(expectedHref).toBe('/subUrl/?orgId=2'); - }); - }); -}); diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index 23549b76c3c..980db44cfc7 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -39,16 +39,7 @@ export class PanelInspector extends PureComponent { // TODO? should we get the result with an observable once? const data = (panel.getQueryRunner() as any).lastResult; return ( - - - {panel.title ? panel.title : 'Panel'} -
  • - } - onDismiss={this.onDismiss} - isOpen={true} - > +
    diff --git a/public/sass/components/_modals.scss b/public/sass/components/_modals.scss index be9fbcd38c0..02b8f5fd3de 100644 --- a/public/sass/components/_modals.scss +++ b/public/sass/components/_modals.scss @@ -46,8 +46,8 @@ .modal-header-title { font-size: $font-size-h3; float: left; - padding-top: $spacer * 0.75; - margin: 0 $spacer * 3 0 $spacer * 1.5; + padding-top: $space-sm; + margin: 0 $space-md; .gicon { position: relative;