Merge pull request #860 from grafana/rares/ui-improvements-9.3
Rares/UI improvements 9.3
This commit is contained in:
commit
fdbcd43a70
27 changed files with 736 additions and 556 deletions
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import { PluginPageProps, PluginPage as RealPluginPage } from '@grafana/runtime';
|
||||
import Header from 'navbar/Header/Header';
|
||||
|
||||
import { pages } from 'pages';
|
||||
import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { useQueryParams } from 'utils/hooks';
|
||||
|
|
@ -18,6 +19,7 @@ function RealPlugin(props: PluginPageProps): React.ReactNode {
|
|||
return (
|
||||
<RealPluginPage {...props}>
|
||||
<Header page={page} backendLicense={store.backendLicense} />
|
||||
<h3 className="page-title">{pages[page].text}</h3>
|
||||
{props.children}
|
||||
</RealPluginPage>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Label, Button, HorizontalGroup, VerticalGroup, Select, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { capitalCase } from 'change-case';
|
||||
import cn from 'classnames/bind';
|
||||
|
|
@ -19,6 +18,7 @@ import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_
|
|||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { makeRequest } from 'network';
|
||||
import { UserAction } from 'state/userAction';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
||||
import styles from './AlertTemplatesForm.module.css';
|
||||
|
||||
|
|
@ -162,9 +162,7 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
|
|||
) : null}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
const handleGoToTemplateSettingsCllick = () => {
|
||||
getLocationSrv().update({ partial: true, query: { tab: 'Autoresolve' } });
|
||||
};
|
||||
const handleGoToTemplateSettingsCllick = () => LocationHelper.update({ tab: 'Autoresolve' }, 'partial');
|
||||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default function PageErrorHandlingWrapper({
|
|||
objectName?: string;
|
||||
pageName: string;
|
||||
itemNotFoundMessage?: string;
|
||||
children: React.ReactNode;
|
||||
children: () => React.ReactNode;
|
||||
}): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!errorData) {
|
||||
|
|
@ -51,7 +51,7 @@ export default function PageErrorHandlingWrapper({
|
|||
const store = useStore();
|
||||
|
||||
if (!errorData || !errorData.isWrongTeamError) {
|
||||
return <>{children}</>;
|
||||
return <>{children()}</>;
|
||||
}
|
||||
|
||||
const currentTeamId = store.userStore.currentUser?.current_team;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import plugin from '../../../package.json'; // eslint-disable-line
|
||||
import React, { FC, useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Alert } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -12,6 +11,7 @@ import { getIfChatOpsConnected } from 'containers/DefaultPageLayout/helper';
|
|||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserAction } from 'state/userAction';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { GRAFANA_LICENSE_OSS } from 'utils/consts';
|
||||
import { useForceUpdate } from 'utils/hooks';
|
||||
import { getItem, setItem } from 'utils/localStorage';
|
||||
|
|
@ -46,7 +46,7 @@ const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
|
|||
if (query.slack_error) {
|
||||
setShowSlackInstallAlert(query.slack_error);
|
||||
|
||||
getLocationSrv().update({ partial: true, query: { slack_error: undefined }, replace: true });
|
||||
LocationHelper.update({ slack_error: undefined }, 'replace');
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Drawer, Tab, TabContent, TabsBar, Button, VerticalGroup, Input } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -15,6 +14,7 @@ import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_
|
|||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openNotification } from 'utils';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
||||
import { IntegrationSettingsTab } from './IntegrationSettings.types';
|
||||
import Autoresolve from './parts/Autoresolve';
|
||||
|
|
@ -46,7 +46,7 @@ const IntegrationSettings = observer((props: IntegrationSettingsProps) => {
|
|||
const getTabClickHandler = useCallback((tab: IntegrationSettingsTab) => {
|
||||
return () => {
|
||||
setActiveTab(tab);
|
||||
getLocationSrv().update({ partial: true, query: { tab: tab } });
|
||||
LocationHelper.update({ tab }, 'partial');
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ const IntegrationSettings = observer((props: IntegrationSettingsProps) => {
|
|||
|
||||
useEffect(() => {
|
||||
setActiveTab(startTab || IntegrationSettingsTab.Templates);
|
||||
getLocationSrv().update({ partial: true, query: { tab: startTab || IntegrationSettingsTab.Templates } });
|
||||
LocationHelper.update({ tab: startTab || IntegrationSettingsTab.Templates }, 'partial');
|
||||
}, [startTab]);
|
||||
|
||||
const integration = alertReceiveChannelStore.getIntegration(alertReceiveChannel);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Alert, Button, Icon, Label, Modal, Select } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { get } from 'lodash-es';
|
||||
|
|
@ -15,6 +14,7 @@ import { Team } from 'models/team/team.types';
|
|||
import { useStore } from 'state/useStore';
|
||||
import { UserAction } from 'state/userAction';
|
||||
import { openErrorNotification, openNotification } from 'utils';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
||||
import styles from 'containers/IntegrationSettings/parts/Autoresolve.module.css';
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ const Autoresolve = ({ alertReceiveChannelId, onSwitchToTemplate, alertGroupId }
|
|||
};
|
||||
|
||||
const handleGoToTemplateSettingsCllick = () => {
|
||||
getLocationSrv().update({ partial: true, query: { tab: 'Templates' } });
|
||||
LocationHelper.update({ tab: 'Templates' }, 'partial');
|
||||
onSwitchToTemplate('resolve_condition_template');
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
padding-bottom: 36px;
|
||||
}
|
||||
|
||||
.scrollbar-view [class*='-page-header'] {
|
||||
.scrollbar-view h1:first-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
max-width: unset !important;
|
||||
flex-grow: unset !important;
|
||||
flex-basis: unset !important;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.page-scrollbar-content > div:first-child {
|
||||
|
|
@ -29,6 +30,7 @@
|
|||
|
||||
.page-header__title {
|
||||
padding-top: 0 !important;
|
||||
font-size: 2rem !important;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
|
|
|||
3
grafana-plugin/src/navbar/LegacyNavTabsBar.module.scss
Normal file
3
grafana-plugin/src/navbar/LegacyNavTabsBar.module.scss
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.root {
|
||||
min-width: 1500px;
|
||||
}
|
||||
|
|
@ -2,10 +2,15 @@ import React from 'react';
|
|||
|
||||
import { IconName } from '@grafana/data';
|
||||
import { Tab, TabsBar } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import { pages } from 'pages';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './LegacyNavTabsBar.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
export default function LegacyNavTabsBar({ currentPage }: { currentPage: string }): JSX.Element {
|
||||
const store = useStore();
|
||||
|
||||
|
|
@ -14,7 +19,7 @@ export default function LegacyNavTabsBar({ currentPage }: { currentPage: string
|
|||
.filter((page) => (page.hideFromTabsFn ? !page.hideFromTabsFn(store) : !page.hideFromTabs));
|
||||
|
||||
return (
|
||||
<TabsBar>
|
||||
<TabsBar className={cx('root')}>
|
||||
{navigationPages.map((page, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Button, HorizontalGroup, Icon, IconButton, LoadingPlaceholder, Tooltip, VerticalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
import { AppRootProps } from 'types';
|
||||
|
||||
import Collapse from 'components/Collapse/Collapse';
|
||||
import EscalationsFilters from 'components/EscalationsFilters/EscalationsFilters';
|
||||
|
|
@ -28,15 +26,16 @@ import EscalationChainSteps from 'containers/EscalationChainSteps/EscalationChai
|
|||
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
|
||||
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
|
||||
import { pages } from 'pages';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { UserAction } from 'state/userAction';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
||||
import styles from './EscalationChains.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface EscalationChainsPageProps extends WithStoreProps, AppRootProps {}
|
||||
interface EscalationChainsPageProps extends WithStoreProps, PageProps {}
|
||||
|
||||
interface EscalationChainsPageState extends PageBaseState {
|
||||
escalationChainsFilters: { searchTerm: string };
|
||||
|
|
@ -103,7 +102,7 @@ class EscalationChainsPage extends React.Component<EscalationChainsPageProps, Es
|
|||
const { escalationChainStore } = store;
|
||||
|
||||
this.setState({ selectedEscalationChain: escalationChain }, () => {
|
||||
getLocationSrv().update({ partial: true, query: { id: escalationChain } });
|
||||
LocationHelper.update({ id: escalationChain }, 'partial');
|
||||
if (escalationChain) {
|
||||
escalationChainStore.updateEscalationChainDetails(escalationChain);
|
||||
}
|
||||
|
|
@ -143,79 +142,81 @@ class EscalationChainsPage extends React.Component<EscalationChainsPageProps, Es
|
|||
pageName="escalations"
|
||||
itemNotFoundMessage={`Escalation chain with id=${query?.id} is not found. Please select escalation chain from the list.`}
|
||||
>
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('filters')}>
|
||||
<EscalationsFilters value={escalationChainsFilters} onChange={this.handleEscalationsFiltersChange} />
|
||||
</div>
|
||||
{!searchResult || searchResult.length ? (
|
||||
<div className={cx('escalations')}>
|
||||
<div className={cx('left-column')}>
|
||||
<WithPermissionControl userAction={UserAction.UpdateAlertReceiveChannels}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.setState({ showCreateEscalationChainModal: true });
|
||||
}}
|
||||
icon="plus"
|
||||
className={cx('new-escalation-chain')}
|
||||
>
|
||||
New escalation chain
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
<div className={cx('escalations-list')}>
|
||||
{searchResult ? (
|
||||
<GList
|
||||
autoScroll
|
||||
selectedId={selectedEscalationChain}
|
||||
items={searchResult}
|
||||
itemKey="id"
|
||||
onSelect={this.setSelectedEscalationChain}
|
||||
>
|
||||
{(item) => <EscalationChainCard id={item.id} />}
|
||||
</GList>
|
||||
) : (
|
||||
<LoadingPlaceholder className={cx('loading')} text="Loading..." />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('escalation')}>{this.renderEscalation()}</div>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('filters')}>
|
||||
<EscalationsFilters value={escalationChainsFilters} onChange={this.handleEscalationsFiltersChange} />
|
||||
</div>
|
||||
) : (
|
||||
<Tutorial
|
||||
step={TutorialStep.Escalations}
|
||||
title={
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<Text type="secondary">No escalations found, check your filtering and current team.</Text>
|
||||
<WithPermissionControl userAction={UserAction.UpdateEscalationPolicies}>
|
||||
{!searchResult || searchResult.length ? (
|
||||
<div className={cx('escalations')}>
|
||||
<div className={cx('left-column')}>
|
||||
<WithPermissionControl userAction={UserAction.UpdateAlertReceiveChannels}>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
this.setState({ showCreateEscalationChainModal: true });
|
||||
}}
|
||||
icon="plus"
|
||||
className={cx('new-escalation-chain')}
|
||||
>
|
||||
New Escalation Chain
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
</VerticalGroup>
|
||||
}
|
||||
<div className={cx('escalations-list')}>
|
||||
{searchResult ? (
|
||||
<GList
|
||||
autoScroll
|
||||
selectedId={selectedEscalationChain}
|
||||
items={searchResult}
|
||||
itemKey="id"
|
||||
onSelect={this.setSelectedEscalationChain}
|
||||
>
|
||||
{(item) => <EscalationChainCard id={item.id} />}
|
||||
</GList>
|
||||
) : (
|
||||
<LoadingPlaceholder className={cx('loading')} text="Loading..." />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('escalation')}>{this.renderEscalation()}</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tutorial
|
||||
step={TutorialStep.Escalations}
|
||||
title={
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<Text type="secondary">No escalations found, check your filtering and current team.</Text>
|
||||
<WithPermissionControl userAction={UserAction.UpdateEscalationPolicies}>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
this.setState({ showCreateEscalationChainModal: true });
|
||||
}}
|
||||
>
|
||||
New Escalation Chain
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
</VerticalGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showCreateEscalationChainModal && (
|
||||
<EscalationChainForm
|
||||
escalationChainId={escalationChainIdToCopy}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
showCreateEscalationChainModal: false,
|
||||
escalationChainIdToCopy: undefined,
|
||||
});
|
||||
}}
|
||||
onUpdate={this.handleEscalationChainCreate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showCreateEscalationChainModal && (
|
||||
<EscalationChainForm
|
||||
escalationChainId={escalationChainIdToCopy}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
showCreateEscalationChainModal: false,
|
||||
escalationChainIdToCopy: undefined,
|
||||
});
|
||||
}}
|
||||
onUpdate={this.handleEscalationChainCreate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
</PluginPage>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useState, SyntheticEvent } from 'react';
|
||||
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import {
|
||||
Button,
|
||||
HorizontalGroup,
|
||||
|
|
@ -22,7 +21,6 @@ import moment from 'moment-timezone';
|
|||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import Emoji from 'react-emoji-render';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
import { AppRootProps } from 'types';
|
||||
|
||||
import Collapse from 'components/Collapse/Collapse';
|
||||
import Block from 'components/GBlock/Block';
|
||||
|
|
@ -49,12 +47,12 @@ import {
|
|||
} from 'models/alertgroup/alertgroup.types';
|
||||
import { ResolutionNoteSourceTypesToDisplayName } from 'models/resolution_note/resolution_note.types';
|
||||
import { pages } from 'pages';
|
||||
import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserAction } from 'state/userAction';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { openNotification } from 'utils';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import sanitize from 'utils/sanitize';
|
||||
|
||||
import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from './Incident.helpers';
|
||||
|
|
@ -63,7 +61,7 @@ import styles from './Incident.module.css';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface IncidentPageProps extends WithStoreProps, AppRootProps {}
|
||||
interface IncidentPageProps extends WithStoreProps, PageProps {}
|
||||
|
||||
interface IncidentPageState extends PageBaseState {
|
||||
showIntegrationSettings?: boolean;
|
||||
|
|
@ -97,8 +95,10 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
update = () => {
|
||||
this.setState({ errorData: initErrorDataState() }); // reset wrong team error to false
|
||||
|
||||
const { store } = this.props;
|
||||
const { id } = getQueryParams();
|
||||
const {
|
||||
store,
|
||||
query: { id },
|
||||
} = this.props;
|
||||
|
||||
store.alertGroupStore
|
||||
.getAlert(id)
|
||||
|
|
@ -106,8 +106,10 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
};
|
||||
|
||||
render() {
|
||||
const { store } = this.props;
|
||||
const { id, cursor, start, perpage } = getQueryParams();
|
||||
const {
|
||||
store,
|
||||
query: { id, cursor, start, perpage },
|
||||
} = this.props;
|
||||
|
||||
const { errorData, showIntegrationSettings, showAttachIncidentForm } = this.state;
|
||||
const { isNotFoundError, isWrongTeamError } = errorData;
|
||||
|
|
@ -127,74 +129,78 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
return (
|
||||
<PluginPage pageNav={pages['incident'].getPageNav()}>
|
||||
<PageErrorHandlingWrapper errorData={errorData} objectName="alert group" pageName="incidents">
|
||||
<div className={cx('root')}>
|
||||
{errorData.isNotFoundError ? (
|
||||
<div className={cx('not-found')}>
|
||||
<VerticalGroup spacing="lg" align="center">
|
||||
<Text.Title level={1}>404</Text.Title>
|
||||
<Text.Title level={4}>Incident not found</Text.Title>
|
||||
<PluginLink query={{ page: 'incidents', cursor, start, perpage }}>
|
||||
<Button variant="secondary" icon="arrow-left" size="md">
|
||||
Go to incidents page
|
||||
</Button>
|
||||
</PluginLink>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{this.renderHeader()}
|
||||
<div className={cx('content')}>
|
||||
<div className={cx('column')}>
|
||||
<Incident incident={incident} datetimeReference={this.getIncidentDatetimeReference(incident)} />
|
||||
<GroupedIncidentsList
|
||||
id={incident.pk}
|
||||
getIncidentDatetimeReference={this.getIncidentDatetimeReference}
|
||||
/>
|
||||
<AttachedIncidentsList id={incident.pk} getUnattachClickHandler={this.getUnattachClickHandler} />
|
||||
</div>
|
||||
<div className={cx('column')}>{this.renderTimeline()}</div>
|
||||
{() => (
|
||||
<div className={cx('root')}>
|
||||
{errorData.isNotFoundError ? (
|
||||
<div className={cx('not-found')}>
|
||||
<VerticalGroup spacing="lg" align="center">
|
||||
<Text.Title level={1}>404</Text.Title>
|
||||
<Text.Title level={4}>Incident not found</Text.Title>
|
||||
<PluginLink query={{ page: 'incidents', cursor, start, perpage }}>
|
||||
<Button variant="secondary" icon="arrow-left" size="md">
|
||||
Go to incidents page
|
||||
</Button>
|
||||
</PluginLink>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
{showIntegrationSettings && (
|
||||
<IntegrationSettings
|
||||
alertGroupId={incident.pk}
|
||||
onUpdate={() => {
|
||||
alertReceiveChannelStore.updateItem(incident.alert_receive_channel.id);
|
||||
}}
|
||||
onUpdateTemplates={() => {
|
||||
store.alertGroupStore.getAlert(id);
|
||||
}}
|
||||
startTab={IntegrationSettingsTab.Templates}
|
||||
id={incident.alert_receive_channel.id}
|
||||
onHide={() =>
|
||||
this.setState({
|
||||
showIntegrationSettings: undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showAttachIncidentForm && (
|
||||
<AttachIncidentForm
|
||||
id={id}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
showAttachIncidentForm: false,
|
||||
});
|
||||
}}
|
||||
onUpdate={this.update}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{this.renderHeader()}
|
||||
<div className={cx('content')}>
|
||||
<div className={cx('column')}>
|
||||
<Incident incident={incident} datetimeReference={this.getIncidentDatetimeReference(incident)} />
|
||||
<GroupedIncidentsList
|
||||
id={incident.pk}
|
||||
getIncidentDatetimeReference={this.getIncidentDatetimeReference}
|
||||
/>
|
||||
<AttachedIncidentsList id={incident.pk} getUnattachClickHandler={this.getUnattachClickHandler} />
|
||||
</div>
|
||||
<div className={cx('column')}>{this.renderTimeline()}</div>
|
||||
</div>
|
||||
{showIntegrationSettings && (
|
||||
<IntegrationSettings
|
||||
alertGroupId={incident.pk}
|
||||
onUpdate={() => {
|
||||
alertReceiveChannelStore.updateItem(incident.alert_receive_channel.id);
|
||||
}}
|
||||
onUpdateTemplates={() => {
|
||||
store.alertGroupStore.getAlert(id);
|
||||
}}
|
||||
startTab={IntegrationSettingsTab.Templates}
|
||||
id={incident.alert_receive_channel.id}
|
||||
onHide={() =>
|
||||
this.setState({
|
||||
showIntegrationSettings: undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showAttachIncidentForm && (
|
||||
<AttachIncidentForm
|
||||
id={id}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
showAttachIncidentForm: false,
|
||||
});
|
||||
}}
|
||||
onUpdate={this.update}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
</PluginPage>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeader = () => {
|
||||
const { store } = this.props;
|
||||
const {
|
||||
store,
|
||||
query: { id, cursor, start, perpage },
|
||||
} = this.props;
|
||||
|
||||
const { id, cursor, start, perpage } = getQueryParams();
|
||||
const { alerts } = store.alertGroupStore;
|
||||
|
||||
const incident = alerts.get(id);
|
||||
|
|
@ -311,9 +317,11 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
};
|
||||
|
||||
renderTimeline = () => {
|
||||
const { store } = this.props;
|
||||
const {
|
||||
store,
|
||||
query: { id },
|
||||
} = this.props;
|
||||
|
||||
const { id } = getQueryParams();
|
||||
const incident = store.alertGroupStore.alerts.get(id);
|
||||
|
||||
if (!incident.render_after_resolve_report_json) {
|
||||
|
|
@ -401,9 +409,11 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
};
|
||||
|
||||
handleCreateResolutionNote = () => {
|
||||
const { store } = this.props;
|
||||
const {
|
||||
store,
|
||||
query: { id },
|
||||
} = this.props;
|
||||
|
||||
const { id } = getQueryParams();
|
||||
const { resolutionNoteText } = this.state;
|
||||
store.resolutionNotesStore
|
||||
.createResolutionNote(id, resolutionNoteText)
|
||||
|
|
@ -419,9 +429,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
case 'author':
|
||||
return (
|
||||
<span
|
||||
onClick={() => {
|
||||
getLocationSrv().update({ query: { page: 'users', id: entity?.author?.pk } });
|
||||
}}
|
||||
onClick={() => LocationHelper.update({ id: entity?.author?.pk, page: 'users' }, 'replace')}
|
||||
style={{ textDecoration: 'underline', cursor: 'pointer' }}
|
||||
>
|
||||
{entity.author?.username}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { ReactElement, SyntheticEvent } from 'react';
|
||||
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Button, Icon, Tooltip, VerticalGroup, LoadingPlaceholder, HorizontalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
|
|
@ -8,12 +7,10 @@ import { get } from 'lodash-es';
|
|||
import { observer } from 'mobx-react';
|
||||
import moment from 'moment-timezone';
|
||||
import Emoji from 'react-emoji-render';
|
||||
import { AppRootProps } from 'types';
|
||||
|
||||
import CursorPagination from 'components/CursorPagination/CursorPagination';
|
||||
import GTable from 'components/GTable/GTable';
|
||||
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
|
||||
import PageErrorHandlingWrapper from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import Tutorial from 'components/Tutorial/Tutorial';
|
||||
|
|
@ -25,11 +22,11 @@ import { Alert, Alert as AlertType, AlertAction } from 'models/alertgroup/alertg
|
|||
import { User } from 'models/user/user.types';
|
||||
import { pages } from 'pages';
|
||||
import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from 'pages/incident/Incident.helpers';
|
||||
import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers';
|
||||
import { move } from 'state/helpers';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { UserAction } from 'state/userAction';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
||||
import SilenceDropdown from './parts/SilenceDropdown';
|
||||
|
||||
|
|
@ -54,7 +51,7 @@ function withSkeleton(fn: (alert: AlertType) => ReactElement | ReactElement[]) {
|
|||
return WithSkeleton;
|
||||
}
|
||||
|
||||
interface IncidentsPageProps extends WithStoreProps, AppRootProps {}
|
||||
interface IncidentsPageProps extends WithStoreProps, PageProps {}
|
||||
|
||||
interface IncidentsPageState {
|
||||
selectedIncidentIds: Array<Alert['pk']>;
|
||||
|
|
@ -71,8 +68,10 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
constructor(props: IncidentsPageProps) {
|
||||
super(props);
|
||||
|
||||
const { store } = props;
|
||||
const { cursor: cursorQuery, start: startQuery, perpage: perpageQuery } = getQueryParams();
|
||||
const {
|
||||
store,
|
||||
query: { cursor: cursorQuery, start: startQuery, perpage: perpageQuery },
|
||||
} = props;
|
||||
|
||||
const cursor = cursorQuery || undefined;
|
||||
const start = !isNaN(startQuery) ? Number(startQuery) : 1;
|
||||
|
|
@ -103,12 +102,10 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
render() {
|
||||
return (
|
||||
<PluginPage pageNav={pages['incidents'].getPageNav()}>
|
||||
<PageErrorHandlingWrapper pageName="incidents">
|
||||
<div className={cx('root')}>
|
||||
{this.renderIncidentFilters()}
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</PageErrorHandlingWrapper>
|
||||
<div className={cx('root')}>
|
||||
{this.renderIncidentFilters()}
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
</PluginPage>
|
||||
);
|
||||
}
|
||||
|
|
@ -148,7 +145,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
fetchIncidentData = (filters: IncidentsFiltersType, isOnMount: boolean) => {
|
||||
const { store } = this.props;
|
||||
store.alertGroupStore.updateIncidentFilters(filters, isOnMount); // this line fetches incidents
|
||||
getLocationSrv().update({ query: { page: 'incidents', ...store.alertGroupStore.incidentFilters } });
|
||||
LocationHelper.update({ page: 'incidents', ...store.alertGroupStore.incidentFilters }, 'partial');
|
||||
};
|
||||
|
||||
onChangeCursor = (cursor: string, direction: 'prev' | 'next') => {
|
||||
|
|
@ -577,7 +574,9 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
|||
}
|
||||
|
||||
setPollingInterval(filters: IncidentsFiltersType = this.state.filters, isOnMount = false) {
|
||||
this.pollingIntervalId = setInterval(() => this.fetchIncidentData(filters, isOnMount), POLLING_NUM_SECONDS * 1000);
|
||||
this.pollingIntervalId = setInterval(() => {
|
||||
this.fetchIncidentData(filters, isOnMount);
|
||||
}, POLLING_NUM_SECONDS * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export const pages: { [id: string]: PageDefinition } = [
|
|||
{
|
||||
icon: 'cog',
|
||||
id: 'settings',
|
||||
text: 'Organization Settings',
|
||||
text: 'Settings',
|
||||
hideFromBreadcrumbs: true,
|
||||
path: getPath('settings'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Button, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
import { AppRootProps } from 'types';
|
||||
|
||||
import GList from 'components/GList/GList';
|
||||
import IntegrationsFilters, { Filters } from 'components/IntegrationsFilters/IntegrationsFilters';
|
||||
|
|
@ -27,9 +25,10 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm
|
|||
import { AlertReceiveChannel } from 'models/alert_receive_channel';
|
||||
import { AlertReceiveChannelOption } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { pages } from 'pages';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { UserAction } from 'state/userAction';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
||||
import styles from './Integrations.module.css';
|
||||
|
||||
|
|
@ -42,7 +41,7 @@ interface IntegrationsState extends PageBaseState {
|
|||
integrationSettingsTab?: IntegrationSettingsTab;
|
||||
}
|
||||
|
||||
interface IntegrationsProps extends WithStoreProps, AppRootProps {}
|
||||
interface IntegrationsProps extends WithStoreProps, PageProps {}
|
||||
|
||||
@observer
|
||||
class Integrations extends React.Component<IntegrationsProps, IntegrationsState> {
|
||||
|
|
@ -62,7 +61,7 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
setSelectedAlertReceiveChannel = (alertReceiveChannelId: AlertReceiveChannel['id']) => {
|
||||
const { store } = this.props;
|
||||
store.selectedAlertReceiveChannel = alertReceiveChannelId;
|
||||
getLocationSrv().update({ partial: true, query: { id: alertReceiveChannelId } });
|
||||
LocationHelper.update({ id: alertReceiveChannelId }, 'partial');
|
||||
};
|
||||
|
||||
parseQueryParams = async () => {
|
||||
|
|
@ -139,110 +138,112 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
|||
pageName="integrations"
|
||||
itemNotFoundMessage={`Integration with id=${query?.id} is not found. Please select integration from the list.`}
|
||||
>
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('filters')}>
|
||||
<IntegrationsFilters value={integrationsFilters} onChange={this.handleIntegrationsFiltersChange} />
|
||||
</div>
|
||||
{searchResult?.length ? (
|
||||
<div className={cx('integrations')}>
|
||||
<div className={cx('integrationsList')}>
|
||||
<WithPermissionControl userAction={UserAction.UpdateAlertReceiveChannels}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.setState({ showCreateIntegrationModal: true });
|
||||
}}
|
||||
icon="plus"
|
||||
className={cx('newIntegrationButton')}
|
||||
>
|
||||
New integration for receiving alerts
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
<div className={cx('alert-receive-channels-list')}>
|
||||
<GList
|
||||
autoScroll
|
||||
selectedId={store.selectedAlertReceiveChannel}
|
||||
items={searchResult}
|
||||
itemKey="id"
|
||||
onSelect={this.handleAlertReceiveChannelSelect}
|
||||
>
|
||||
{(item) => (
|
||||
<AlertReceiveChannelCard
|
||||
id={item.id}
|
||||
onShowHeartbeatModal={() => {
|
||||
this.setState({
|
||||
alertReceiveChannelToShowSettings: item.id,
|
||||
integrationSettingsTab: IntegrationSettingsTab.Heartbeat,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</GList>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('alert-rules', 'alertRulesBorder')}>
|
||||
<AlertRules
|
||||
alertReceiveChannelId={store.selectedAlertReceiveChannel}
|
||||
onDelete={this.handleDeleteAlertReceiveChannel}
|
||||
onShowSettings={(integrationSettingsTab?: IntegrationSettingsTab) => {
|
||||
this.setState({
|
||||
alertReceiveChannelToShowSettings: store.selectedAlertReceiveChannel,
|
||||
integrationSettingsTab,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('filters')}>
|
||||
<IntegrationsFilters value={integrationsFilters} onChange={this.handleIntegrationsFiltersChange} />
|
||||
</div>
|
||||
) : searchResult ? (
|
||||
<Tutorial
|
||||
step={TutorialStep.Integrations}
|
||||
title={
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<Text type="secondary">No integrations found. Review your filter and team settings.</Text>
|
||||
{searchResult?.length ? (
|
||||
<div className={cx('integrations')}>
|
||||
<div className={cx('integrationsList')}>
|
||||
<WithPermissionControl userAction={UserAction.UpdateAlertReceiveChannels}>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
this.setState({ showCreateIntegrationModal: true });
|
||||
}}
|
||||
icon="plus"
|
||||
className={cx('newIntegrationButton')}
|
||||
>
|
||||
New integration for receiving alerts
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
</VerticalGroup>
|
||||
}
|
||||
<div className={cx('alert-receive-channels-list')}>
|
||||
<GList
|
||||
autoScroll
|
||||
selectedId={store.selectedAlertReceiveChannel}
|
||||
items={searchResult}
|
||||
itemKey="id"
|
||||
onSelect={this.handleAlertReceiveChannelSelect}
|
||||
>
|
||||
{(item) => (
|
||||
<AlertReceiveChannelCard
|
||||
id={item.id}
|
||||
onShowHeartbeatModal={() => {
|
||||
this.setState({
|
||||
alertReceiveChannelToShowSettings: item.id,
|
||||
integrationSettingsTab: IntegrationSettingsTab.Heartbeat,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</GList>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('alert-rules', 'alertRulesBorder')}>
|
||||
<AlertRules
|
||||
alertReceiveChannelId={store.selectedAlertReceiveChannel}
|
||||
onDelete={this.handleDeleteAlertReceiveChannel}
|
||||
onShowSettings={(integrationSettingsTab?: IntegrationSettingsTab) => {
|
||||
this.setState({
|
||||
alertReceiveChannelToShowSettings: store.selectedAlertReceiveChannel,
|
||||
integrationSettingsTab,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : searchResult ? (
|
||||
<Tutorial
|
||||
step={TutorialStep.Integrations}
|
||||
title={
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<Text type="secondary">No integrations found. Review your filter and team settings.</Text>
|
||||
<WithPermissionControl userAction={UserAction.UpdateAlertReceiveChannels}>
|
||||
<Button
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
this.setState({ showCreateIntegrationModal: true });
|
||||
}}
|
||||
>
|
||||
New integration for receiving alerts
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
</VerticalGroup>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
)}
|
||||
</div>
|
||||
{alertReceiveChannelToShowSettings && (
|
||||
<IntegrationSettings
|
||||
onUpdate={() => {
|
||||
alertReceiveChannelStore.updateItem(alertReceiveChannelToShowSettings);
|
||||
}}
|
||||
startTab={integrationSettingsTab}
|
||||
id={alertReceiveChannelToShowSettings}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
alertReceiveChannelToShowSettings: undefined,
|
||||
integrationSettingsTab: undefined,
|
||||
});
|
||||
LocationHelper.update({ tab: undefined }, 'partial');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
)}
|
||||
</div>
|
||||
{alertReceiveChannelToShowSettings && (
|
||||
<IntegrationSettings
|
||||
onUpdate={() => {
|
||||
alertReceiveChannelStore.updateItem(alertReceiveChannelToShowSettings);
|
||||
}}
|
||||
startTab={integrationSettingsTab}
|
||||
id={alertReceiveChannelToShowSettings}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
alertReceiveChannelToShowSettings: undefined,
|
||||
integrationSettingsTab: undefined,
|
||||
});
|
||||
getLocationSrv().update({ partial: true, query: { tab: undefined } });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showCreateIntegrationModal && (
|
||||
<CreateAlertReceiveChannelContainer
|
||||
onHide={() => {
|
||||
this.setState({ showCreateIntegrationModal: false });
|
||||
}}
|
||||
onCreate={this.handleCreateNewAlertReceiveChannel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{showCreateIntegrationModal && (
|
||||
<CreateAlertReceiveChannelContainer
|
||||
onHide={() => {
|
||||
this.setState({ showCreateIntegrationModal: false });
|
||||
}}
|
||||
onCreate={this.handleCreateNewAlertReceiveChannel}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
</PluginPage>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { observer } from 'mobx-react';
|
|||
import moment from 'moment-timezone';
|
||||
import LegacyNavHeading from 'navbar/LegacyNavHeading';
|
||||
import Emoji from 'react-emoji-render';
|
||||
import { AppRootProps } from 'types';
|
||||
|
||||
import GTable from 'components/GTable/GTable';
|
||||
import Text from 'components/Text/Text';
|
||||
|
|
@ -18,7 +17,7 @@ import { getAlertReceiveChannelDisplayName } from 'models/alert_receive_channel/
|
|||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { Maintenance, MaintenanceMode, MaintenanceType } from 'models/maintenance/maintenance.types';
|
||||
import { pages } from 'pages';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { UserAction } from 'state/userAction';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
|
||||
|
|
@ -26,7 +25,7 @@ import styles from './Maintenance.module.css';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface MaintenancePageProps extends AppRootProps, WithStoreProps {}
|
||||
interface MaintenancePageProps extends PageProps, WithStoreProps {}
|
||||
|
||||
interface MaintenancePageState {
|
||||
maintenanceData?: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import 'jest/matchMedia.ts';
|
||||
import React from 'react';
|
||||
|
||||
import { describe, expect, test } from '@jest/globals';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import outgoingWebhooksStub from 'jest/outgoingWebhooksStub';
|
||||
|
||||
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { OutgoingWebhooks } from 'pages/outgoing_webhooks/OutgoingWebhooks';
|
||||
|
||||
const outgoingWebhooks = outgoingWebhooksStub as OutgoingWebhook[];
|
||||
const outgoingWebhookStore = () => ({
|
||||
loadItem: () => Promise.resolve(outgoingWebhooks[0]),
|
||||
updateItems: () => Promise.resolve(),
|
||||
getSearchResult: () => outgoingWebhooks,
|
||||
items: outgoingWebhooks.reduce((prev, current) => {
|
||||
prev[current.id] = current;
|
||||
return prev;
|
||||
}, {}),
|
||||
});
|
||||
|
||||
jest.mock('plugin/GrafanaPluginRootPage.helpers', () => ({
|
||||
isTopNavbar: () => false,
|
||||
}));
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
config: {
|
||||
featureToggles: {
|
||||
topNav: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('state/useStore', () => ({
|
||||
useStore: () => ({
|
||||
outgoingWebhookStore: outgoingWebhookStore(),
|
||||
isUserActionAllowed: jest.fn().mockReturnValue(true),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
getLocationSrv: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('OutgoingWebhooks', () => {
|
||||
const storeMock = {
|
||||
isUserActionAllowed: jest.fn().mockReturnValue(true),
|
||||
outgoingWebhookStore: outgoingWebhookStore(),
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
console.warn = () => {};
|
||||
console.error = () => {};
|
||||
});
|
||||
|
||||
test('It renders all retrieved webhooks', async () => {
|
||||
render(<OutgoingWebhooks {...getProps()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const gTable = screen.queryByTestId('test__gTable');
|
||||
const rows = gTable.querySelectorAll('tbody tr');
|
||||
|
||||
expect(() => queryEditForm()).toThrow(); // edit doesn't show for [id=undefined]
|
||||
expect(rows.length).toBe(outgoingWebhooks.length);
|
||||
});
|
||||
});
|
||||
|
||||
test('It opens Edit View if [id] is supplied', async () => {
|
||||
const id = outgoingWebhooks[0].id;
|
||||
render(<OutgoingWebhooks {...getProps(id)} />);
|
||||
|
||||
expect(() => queryEditForm()).toThrow(); // before updates kick in
|
||||
await waitFor(() => {
|
||||
expect(queryEditForm()).toBeDefined(); // edit shows for [id=?]
|
||||
});
|
||||
});
|
||||
|
||||
function getProps(id: OutgoingWebhook['id'] = undefined): any {
|
||||
return { store: storeMock, query: { id } };
|
||||
}
|
||||
|
||||
function queryEditForm(): HTMLElement {
|
||||
return screen.getByTestId<HTMLElement>('test__outgoingWebhookEditForm');
|
||||
}
|
||||
});
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Button, HorizontalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import LegacyNavHeading from 'navbar/LegacyNavHeading';
|
||||
import { AppRootProps } from 'types';
|
||||
|
||||
import GTable from 'components/GTable/GTable';
|
||||
import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
|
||||
|
|
@ -22,16 +20,16 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm
|
|||
import { ActionDTO } from 'models/action';
|
||||
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { pages } from 'pages';
|
||||
import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { UserAction } from 'state/userAction';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
||||
import styles from './OutgoingWebhooks.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface OutgoingWebhooksProps extends WithStoreProps, AppRootProps {}
|
||||
interface OutgoingWebhooksProps extends WithStoreProps, PageProps {}
|
||||
|
||||
interface OutgoingWebhooksState extends PageBaseState {
|
||||
outgoingWebhookIdToEdit?: OutgoingWebhook['id'] | 'new';
|
||||
|
|
@ -43,14 +41,12 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
errorData: initErrorDataState(),
|
||||
};
|
||||
|
||||
private outgoingWebhookId: string;
|
||||
|
||||
async componentDidMount() {
|
||||
this.update().then(this.parseQueryParams);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.outgoingWebhookId !== getQueryParams()['id']) {
|
||||
componentDidUpdate(prevProps: OutgoingWebhooksProps) {
|
||||
if (prevProps.query.id !== this.props.query.id) {
|
||||
this.parseQueryParams();
|
||||
}
|
||||
}
|
||||
|
|
@ -61,10 +57,10 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
outgoingWebhookIdToEdit: undefined,
|
||||
})); // reset state on query parse
|
||||
|
||||
const { store } = this.props;
|
||||
const { id } = getQueryParams();
|
||||
|
||||
this.outgoingWebhookId = id;
|
||||
const {
|
||||
store,
|
||||
query: { id },
|
||||
} = this.props;
|
||||
|
||||
if (!id) {
|
||||
return;
|
||||
|
|
@ -122,43 +118,45 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
pageName="outgoing_webhooks"
|
||||
itemNotFoundMessage={`Outgoing webhook with id=${query?.id} is not found. Please select outgoing webhook from the list.`}
|
||||
>
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<GTable
|
||||
emptyText={webhooks ? 'No outgoing webhooks found' : 'Loading...'}
|
||||
title={() => (
|
||||
<div className={cx('header')}>
|
||||
<LegacyNavHeading>
|
||||
<Text.Title level={3}>Outgoing Webhooks</Text.Title>
|
||||
</LegacyNavHeading>
|
||||
<div className="u-pull-right">
|
||||
<PluginLink
|
||||
partial
|
||||
query={{ id: 'new' }}
|
||||
disabled={!store.isUserActionAllowed(UserAction.UpdateCustomActions)}
|
||||
>
|
||||
<WithPermissionControl userAction={UserAction.UpdateCustomActions}>
|
||||
<Button variant="primary" icon="plus">
|
||||
Create
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
</PluginLink>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<GTable
|
||||
emptyText={webhooks ? 'No outgoing webhooks found' : 'Loading...'}
|
||||
title={() => (
|
||||
<div className={cx('header')}>
|
||||
<LegacyNavHeading>
|
||||
<Text.Title level={3}>Outgoing Webhooks</Text.Title>
|
||||
</LegacyNavHeading>
|
||||
<div className="u-pull-right">
|
||||
<PluginLink
|
||||
partial
|
||||
query={{ id: 'new' }}
|
||||
disabled={!store.isUserActionAllowed(UserAction.UpdateCustomActions)}
|
||||
>
|
||||
<WithPermissionControl userAction={UserAction.UpdateCustomActions}>
|
||||
<Button variant="primary" icon="plus">
|
||||
Create
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
</PluginLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
data={webhooks}
|
||||
/>
|
||||
</div>
|
||||
{outgoingWebhookIdToEdit && (
|
||||
<OutgoingWebhookForm
|
||||
id={outgoingWebhookIdToEdit}
|
||||
onUpdate={this.update}
|
||||
onHide={this.handleOutgoingWebhookFormHide}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
data={webhooks}
|
||||
/>
|
||||
</div>
|
||||
{outgoingWebhookIdToEdit && (
|
||||
<OutgoingWebhookForm
|
||||
id={outgoingWebhookIdToEdit}
|
||||
onUpdate={this.update}
|
||||
onHide={this.handleOutgoingWebhookFormHide}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
</PluginPage>
|
||||
);
|
||||
|
|
@ -194,14 +192,14 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
return () => {
|
||||
this.setState({ outgoingWebhookIdToEdit: id });
|
||||
|
||||
getLocationSrv().update({ partial: true, query: { id } });
|
||||
LocationHelper.update({ id }, 'partial');
|
||||
};
|
||||
};
|
||||
|
||||
handleOutgoingWebhookFormHide = () => {
|
||||
this.setState({ outgoingWebhookIdToEdit: undefined });
|
||||
|
||||
getLocationSrv().update({ partial: true, query: { id: undefined } });
|
||||
LocationHelper.update({ id: undefined }, 'partial');
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Button, HorizontalGroup, VerticalGroup, IconButton, ToolbarButton, Icon, Modal } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
import { AppRootProps } from 'types';
|
||||
|
||||
import PageErrorHandlingWrapper from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
|
|
@ -23,10 +21,10 @@ import UsersTimezones from 'containers/UsersTimezones/UsersTimezones';
|
|||
import { Schedule, ScheduleType, Shift } from 'models/schedule/schedule.types';
|
||||
import { Timezone } from 'models/timezone/timezone.types';
|
||||
import { pages } from 'pages';
|
||||
import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { UserAction } from 'state/userAction';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
||||
import { getStartOfWeek } from './Schedule.helpers';
|
||||
|
||||
|
|
@ -34,7 +32,7 @@ import styles from './Schedule.module.css';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface SchedulePageProps extends AppRootProps, WithStoreProps {}
|
||||
interface SchedulePageProps extends PageProps, WithStoreProps {}
|
||||
|
||||
interface SchedulePageState {
|
||||
startMoment: dayjs.Dayjs;
|
||||
|
|
@ -66,8 +64,10 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { store } = this.props;
|
||||
const { id } = getQueryParams();
|
||||
const {
|
||||
store,
|
||||
query: { id },
|
||||
} = this.props;
|
||||
|
||||
store.userStore.updateItems();
|
||||
|
||||
|
|
@ -86,8 +86,10 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
}
|
||||
|
||||
render() {
|
||||
const { store } = this.props;
|
||||
const { id: scheduleId } = getQueryParams();
|
||||
const {
|
||||
store,
|
||||
query: { id: scheduleId },
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
startMoment,
|
||||
|
|
@ -112,139 +114,150 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
return (
|
||||
<PluginPage pageNav={pages['schedule'].getPageNav()}>
|
||||
<PageErrorHandlingWrapper pageName="schedules">
|
||||
<div className={cx('root')}>
|
||||
<VerticalGroup spacing="lg">
|
||||
<div className={cx('header')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<PluginLink query={{ page: 'schedules' }}>
|
||||
<IconButton style={{ marginTop: '5px' }} name="arrow-left" size="xl" />
|
||||
</PluginLink>
|
||||
<Text.Title editable editModalTitle="Schedule name" level={2} onTextChange={this.handleNameChange}>
|
||||
{schedule?.name}
|
||||
</Text.Title>
|
||||
{schedule && <ScheduleWarning item={schedule} />}
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="lg">
|
||||
{users && (
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<VerticalGroup spacing="lg">
|
||||
<div className={cx('header')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">Current timezone:</Text>
|
||||
<UserTimezoneSelect
|
||||
value={currentTimezone}
|
||||
users={users}
|
||||
onChange={this.handleTimezoneChange}
|
||||
/>
|
||||
<PluginLink query={{ page: 'schedules' }}>
|
||||
<IconButton style={{ marginTop: '5px' }} name="arrow-left" size="xl" />
|
||||
</PluginLink>
|
||||
<Text.Title
|
||||
editable
|
||||
editModalTitle="Schedule name"
|
||||
level={2}
|
||||
onTextChange={this.handleNameChange}
|
||||
>
|
||||
{schedule?.name}
|
||||
</Text.Title>
|
||||
{schedule && <ScheduleWarning item={schedule} />}
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={this.handleExportClick()}>
|
||||
Export
|
||||
</Button>
|
||||
{(schedule?.type === ScheduleType.Ical || schedule?.type === ScheduleType.Calendar) && (
|
||||
<Button variant="secondary" onClick={this.handleReloadClick(scheduleId)}>
|
||||
Reload
|
||||
</Button>
|
||||
<HorizontalGroup spacing="lg">
|
||||
{users && (
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">Current timezone:</Text>
|
||||
<UserTimezoneSelect
|
||||
value={currentTimezone}
|
||||
users={users}
|
||||
onChange={this.handleTimezoneChange}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={this.handleExportClick()}>
|
||||
Export
|
||||
</Button>
|
||||
{(schedule?.type === ScheduleType.Ical || schedule?.type === ScheduleType.Calendar) && (
|
||||
<Button variant="secondary" onClick={this.handleReloadClick(scheduleId)}>
|
||||
Reload
|
||||
</Button>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
<ToolbarButton
|
||||
icon="cog"
|
||||
tooltip="Settings"
|
||||
onClick={() => {
|
||||
this.setState({ showEditForm: true });
|
||||
}}
|
||||
/>
|
||||
<WithConfirm>
|
||||
<ToolbarButton icon="trash-alt" tooltip="Delete" onClick={this.handleDelete} />
|
||||
</WithConfirm>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<ToolbarButton
|
||||
icon="cog"
|
||||
tooltip="Settings"
|
||||
onClick={() => {
|
||||
this.setState({ showEditForm: true });
|
||||
}}
|
||||
/>
|
||||
<WithConfirm>
|
||||
<ToolbarButton icon="trash-alt" tooltip="Delete" onClick={this.handleDelete} />
|
||||
</WithConfirm>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('users-timezones')}>
|
||||
<UsersTimezones
|
||||
scheduleId={scheduleId}
|
||||
startMoment={startMoment}
|
||||
onCallNow={schedule?.on_call_now || []}
|
||||
userIds={
|
||||
scheduleStore.relatedUsers[scheduleId] ? Object.keys(scheduleStore.relatedUsers[scheduleId]) : []
|
||||
}
|
||||
tz={currentTimezone}
|
||||
onTzChange={this.handleTimezoneChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('users-timezones')}>
|
||||
<UsersTimezones
|
||||
scheduleId={scheduleId}
|
||||
startMoment={startMoment}
|
||||
onCallNow={schedule?.on_call_now || []}
|
||||
userIds={
|
||||
scheduleStore.relatedUsers[scheduleId]
|
||||
? Object.keys(scheduleStore.relatedUsers[scheduleId])
|
||||
: []
|
||||
}
|
||||
tz={currentTimezone}
|
||||
onTzChange={this.handleTimezoneChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cx('rotations')}>
|
||||
<div className={cx('controls')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={this.handleTodayClick}>
|
||||
Today
|
||||
</Button>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Button variant="secondary" onClick={this.handleLeftClick}>
|
||||
<Icon name="angle-left" />
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={this.handleRightClick}>
|
||||
<Icon name="angle-right" />
|
||||
</Button>
|
||||
<div className={cx('rotations')}>
|
||||
<div className={cx('controls')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={this.handleTodayClick}>
|
||||
Today
|
||||
</Button>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Button variant="secondary" onClick={this.handleLeftClick}>
|
||||
<Icon name="angle-left" />
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={this.handleRightClick}>
|
||||
<Icon name="angle-right" />
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
<Text.Title style={{ marginLeft: '8px' }} level={4} type="primary">
|
||||
{startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
|
||||
</Text.Title>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<Text.Title style={{ marginLeft: '8px' }} level={4} type="primary">
|
||||
{startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
|
||||
</Text.Title>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<ScheduleFinal
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onClick={this.handleShowForm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Rotations
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onCreate={this.handleCreateRotation}
|
||||
onUpdate={this.handleUpdateRotation}
|
||||
onDelete={this.handleDeleteRotation}
|
||||
shiftIdToShowRotationForm={shiftIdToShowRotationForm}
|
||||
onShowRotationForm={this.handleShowRotationForm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<ScheduleOverrides
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onCreate={this.handleCreateOverride}
|
||||
onUpdate={this.handleUpdateOverride}
|
||||
onDelete={this.handleDeleteOverride}
|
||||
shiftIdToShowRotationForm={shiftIdToShowOverridesForm}
|
||||
onShowRotationForm={this.handleShowOverridesForm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<ScheduleFinal
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onClick={this.handleShowForm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Rotations
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onCreate={this.handleCreateRotation}
|
||||
onUpdate={this.handleUpdateRotation}
|
||||
onDelete={this.handleDeleteRotation}
|
||||
shiftIdToShowRotationForm={shiftIdToShowRotationForm}
|
||||
onShowRotationForm={this.handleShowRotationForm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<ScheduleOverrides
|
||||
scheduleId={scheduleId}
|
||||
currentTimezone={currentTimezone}
|
||||
startMoment={startMoment}
|
||||
onCreate={this.handleCreateOverride}
|
||||
onUpdate={this.handleUpdateOverride}
|
||||
onDelete={this.handleDeleteOverride}
|
||||
shiftIdToShowRotationForm={shiftIdToShowOverridesForm}
|
||||
onShowRotationForm={this.handleShowOverridesForm}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
{showEditForm && (
|
||||
<ScheduleForm
|
||||
id={schedule.id}
|
||||
onUpdate={this.update}
|
||||
onHide={() => {
|
||||
this.setState({ showEditForm: false });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showScheduleICalSettings && (
|
||||
<Modal
|
||||
isOpen
|
||||
title="Schedule export"
|
||||
closeOnEscape
|
||||
onDismiss={() => this.setState({ showScheduleICalSettings: false })}
|
||||
>
|
||||
<ScheduleICalSettings id={scheduleId} />
|
||||
</Modal>
|
||||
{showEditForm && (
|
||||
<ScheduleForm
|
||||
id={schedule.id}
|
||||
onUpdate={this.update}
|
||||
onHide={() => {
|
||||
this.setState({ showEditForm: false });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showScheduleICalSettings && (
|
||||
<Modal
|
||||
isOpen
|
||||
title="Schedule export"
|
||||
closeOnEscape
|
||||
onDismiss={() => this.setState({ showScheduleICalSettings: false })}
|
||||
>
|
||||
<ScheduleICalSettings id={scheduleId} />
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
</PluginPage>
|
||||
|
|
@ -293,8 +306,10 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
};
|
||||
|
||||
updateEvents = () => {
|
||||
const { store } = this.props;
|
||||
const { id: scheduleId } = getQueryParams();
|
||||
const {
|
||||
store,
|
||||
query: { id: scheduleId },
|
||||
} = this.props;
|
||||
|
||||
const { startMoment } = this.state;
|
||||
|
||||
|
|
@ -418,12 +433,12 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
|
|||
};
|
||||
|
||||
handleDelete = () => {
|
||||
const { store } = this.props;
|
||||
const { id: scheduleId } = getQueryParams();
|
||||
const {
|
||||
store,
|
||||
query: { id: scheduleId },
|
||||
} = this.props;
|
||||
|
||||
store.scheduleStore.delete(scheduleId).then(() => {
|
||||
getLocationSrv().update({ query: { page: 'schedules' } });
|
||||
});
|
||||
store.scheduleStore.delete(scheduleId).then(() => LocationHelper.update({ page: 'schedules' }, 'replace'));
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Button, HorizontalGroup, IconButton, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
|
|
@ -31,6 +30,7 @@ import { getStartOfWeek } from 'pages/schedule/Schedule.helpers';
|
|||
import { WithStoreProps } from 'state/types';
|
||||
import { UserAction } from 'state/userAction';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
||||
import styles from './Schedules.module.css';
|
||||
|
||||
|
|
@ -210,7 +210,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
|
||||
handleCreateSchedule = (data: Schedule) => {
|
||||
if (data.type === ScheduleType.API) {
|
||||
getLocationSrv().update({ query: { page: 'schedule', id: data.id } });
|
||||
LocationHelper.update({ page: 'schedule', id: data.id }, 'replace');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -259,9 +259,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
};
|
||||
|
||||
getScheduleClickHandler = (scheduleId: Schedule['id']) => {
|
||||
return () => {
|
||||
getLocationSrv().update({ query: { page: 'schedule', id: scheduleId } });
|
||||
};
|
||||
return () => LocationHelper.update({ page: 'schedule', id: scheduleId }, 'replace');
|
||||
};
|
||||
|
||||
renderType = (value: number) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Field, Input, Button, HorizontalGroup, Icon, VerticalGroup, LoadingPlaceholder } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -15,6 +14,7 @@ import { WithStoreProps } from 'state/types';
|
|||
import { useStore } from 'state/useStore';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { openErrorNotification } from 'utils';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
||||
import styles from './CloudPage.module.css';
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ const CloudPage = observer((_props: CloudPageProps) => {
|
|||
variant="secondary"
|
||||
size="sm"
|
||||
className={cx('table-button')}
|
||||
onClick={() => getLocationSrv().update({ query: { page: 'users', p: page, id: user.id } })}
|
||||
onClick={() => LocationHelper.update({ page: 'users', p: page, id: user.id }, 'replace')}
|
||||
>
|
||||
Configure notifications
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getLocationSrv } from '@grafana/runtime';
|
||||
import { Alert, Button, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
|
||||
import { PluginPage } from 'PluginPage';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
import LegacyNavHeading from 'navbar/LegacyNavHeading';
|
||||
import { AppRootProps } from 'types';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import GTable from 'components/GTable/GTable';
|
||||
|
|
@ -24,10 +22,10 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm
|
|||
import { getRole } from 'models/user/user.helpers';
|
||||
import { User as UserType, UserRole } from 'models/user/user.types';
|
||||
import { pages } from 'pages';
|
||||
import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { UserAction } from 'state/userAction';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
|
||||
import { getRealFilters, getUserRowClassNameFn } from './Users.helpers';
|
||||
|
||||
|
|
@ -35,7 +33,7 @@ import styles from './Users.module.css';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface UsersProps extends WithStoreProps, AppRootProps {}
|
||||
interface UsersProps extends WithStoreProps, PageProps {}
|
||||
|
||||
const ITEMS_PER_PAGE = 100;
|
||||
|
||||
|
|
@ -65,10 +63,10 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
|
||||
initialUsersLoaded = false;
|
||||
|
||||
private userId: string;
|
||||
|
||||
async componentDidMount() {
|
||||
const { p } = getQueryParams();
|
||||
const {
|
||||
query: { p },
|
||||
} = this.props;
|
||||
this.setState({ page: p ? Number(p) : 1 }, this.updateUsers);
|
||||
|
||||
this.parseParams();
|
||||
|
|
@ -83,11 +81,11 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
return;
|
||||
}
|
||||
|
||||
getLocationSrv().update({ query: { p: page }, partial: true });
|
||||
LocationHelper.update({ p: page }, 'partial');
|
||||
return await userStore.updateItems(getRealFilters(usersFilters), page);
|
||||
};
|
||||
|
||||
componentDidUpdate() {
|
||||
componentDidUpdate(prevProps: UsersProps) {
|
||||
const { store } = this.props;
|
||||
|
||||
if (!this.initialUsersLoaded && store.isUserActionAllowed(UserAction.ViewOtherUsers)) {
|
||||
|
|
@ -95,7 +93,7 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
this.initialUsersLoaded = true;
|
||||
}
|
||||
|
||||
if (this.userId !== getQueryParams()['id']) {
|
||||
if (prevProps.query.id !== this.props.query.id) {
|
||||
this.parseParams();
|
||||
}
|
||||
}
|
||||
|
|
@ -103,10 +101,10 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
parseParams = async () => {
|
||||
this.setState({ errorData: initErrorDataState() }); // reset wrong team error to false on query parse
|
||||
|
||||
const { store } = this.props;
|
||||
const { id } = getQueryParams();
|
||||
|
||||
this.userId = id;
|
||||
const {
|
||||
store,
|
||||
query: { id },
|
||||
} = this.props;
|
||||
|
||||
if (id) {
|
||||
await (id === 'me' ? store.userStore.loadCurrentUser() : store.userStore.loadUser(String(id), true)).catch(
|
||||
|
|
@ -182,74 +180,76 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
pageName="users"
|
||||
itemNotFoundMessage={`User with id=${query?.id} is not found. Please select user from the list.`}
|
||||
>
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('root', 'TEST-users-page')}>
|
||||
<div className={cx('users-header')}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline' }}>
|
||||
<div>
|
||||
<LegacyNavHeading>
|
||||
<Text.Title level={3}>Users</Text.Title>
|
||||
</LegacyNavHeading>
|
||||
<Text type="secondary">
|
||||
To manage permissions or add users, please visit{' '}
|
||||
<a href="/org/users">Grafana user management</a>
|
||||
</Text>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('root', 'TEST-users-page')}>
|
||||
<div className={cx('users-header')}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline' }}>
|
||||
<div>
|
||||
<LegacyNavHeading>
|
||||
<Text.Title level={3}>Users</Text.Title>
|
||||
</LegacyNavHeading>
|
||||
<Text type="secondary">
|
||||
To manage permissions or add users, please visit{' '}
|
||||
<a href="/org/users">Grafana user management</a>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PluginLink partial query={{ id: 'me' }}>
|
||||
<Button variant="primary" icon="user">
|
||||
View my profile
|
||||
</Button>
|
||||
</PluginLink>
|
||||
</div>
|
||||
{store.isUserActionAllowed(UserAction.ViewOtherUsers) ? (
|
||||
<>
|
||||
<div className={cx('user-filters-container')}>
|
||||
<UsersFilters
|
||||
className={cx('users-filters')}
|
||||
value={usersFilters}
|
||||
onChange={this.handleUsersFiltersChange}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="times"
|
||||
onClick={handleClear}
|
||||
className={cx('searchIntegrationClear')}
|
||||
>
|
||||
Clear filters
|
||||
<PluginLink partial query={{ id: 'me' }}>
|
||||
<Button variant="primary" icon="user">
|
||||
View my profile
|
||||
</Button>
|
||||
</div>
|
||||
</PluginLink>
|
||||
</div>
|
||||
{store.isUserActionAllowed(UserAction.ViewOtherUsers) ? (
|
||||
<>
|
||||
<div className={cx('user-filters-container')}>
|
||||
<UsersFilters
|
||||
className={cx('users-filters')}
|
||||
value={usersFilters}
|
||||
onChange={this.handleUsersFiltersChange}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="times"
|
||||
onClick={handleClear}
|
||||
className={cx('searchIntegrationClear')}
|
||||
>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<GTable
|
||||
emptyText={results ? 'No users found' : 'Loading...'}
|
||||
rowKey="pk"
|
||||
data={results}
|
||||
columns={columns}
|
||||
rowClassName={getUserRowClassNameFn(userPkToEdit, userStore.currentUserPk)}
|
||||
pagination={{
|
||||
page,
|
||||
total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
|
||||
onChange: this.handleChangePage,
|
||||
}}
|
||||
<GTable
|
||||
emptyText={results ? 'No users found' : 'Loading...'}
|
||||
rowKey="pk"
|
||||
data={results}
|
||||
columns={columns}
|
||||
rowClassName={getUserRowClassNameFn(userPkToEdit, userStore.currentUserPk)}
|
||||
pagination={{
|
||||
page,
|
||||
total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
|
||||
onChange: this.handleChangePage,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Alert
|
||||
/* @ts-ignore */
|
||||
title={
|
||||
<>
|
||||
You don't have enough permissions to view other users because you are not Admin.{' '}
|
||||
<PluginLink query={{ page: 'users', id: 'me' }}>Click here</PluginLink> to open your profile
|
||||
</>
|
||||
}
|
||||
severity="info"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Alert
|
||||
/* @ts-ignore */
|
||||
title={
|
||||
<>
|
||||
You don't have enough permissions to view other users because you are not Admin.{' '}
|
||||
<PluginLink query={{ page: 'users', id: 'me' }}>Click here</PluginLink> to open your profile
|
||||
</>
|
||||
}
|
||||
severity="info"
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
{userPkToEdit && <UserSettings id={userPkToEdit} onHide={this.handleHideUserSettings} />}
|
||||
</div>
|
||||
{userPkToEdit && <UserSettings id={userPkToEdit} onHide={this.handleHideUserSettings} />}
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
</PluginPage>
|
||||
);
|
||||
|
|
@ -378,7 +378,7 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
handleHideUserSettings = () => {
|
||||
this.setState({ userPkToEdit: undefined });
|
||||
|
||||
getLocationSrv().update({ partial: true, query: { id: undefined } });
|
||||
LocationHelper.update({ id: undefined }, 'partial');
|
||||
};
|
||||
|
||||
handleUserUpdate = () => {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
"name": "Alert Groups",
|
||||
"path": "/a/grafana-oncall-app/?page=incidents",
|
||||
"role": "Viewer",
|
||||
"defaultNav": true,
|
||||
"addToNav": true
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,17 @@ export function getQueryParams(): any {
|
|||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const result = {};
|
||||
for (const [key, value] of searchParams) {
|
||||
result[key] = value;
|
||||
if (result[key]) {
|
||||
// key already existing, we're handling an array
|
||||
if (!Array.isArray(result[key])) {
|
||||
result[key] = new Array(result[key]);
|
||||
}
|
||||
|
||||
result[key].push(value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import 'style/vars.css';
|
|||
import 'style/global.css';
|
||||
import 'style/utils.css';
|
||||
|
||||
import { isTopNavbar } from './GrafanaPluginRootPage.helpers';
|
||||
import { getQueryParams, isTopNavbar } from './GrafanaPluginRootPage.helpers';
|
||||
import PluginSetup from './PluginSetup';
|
||||
|
||||
export const GrafanaPluginRootPage = (props: AppRootProps) => (
|
||||
|
|
@ -101,7 +101,7 @@ export const Root = observer((props: AppRootProps) => {
|
|||
'u-position-relative'
|
||||
)}
|
||||
>
|
||||
<Page {...props} path={pathWithoutLeadingSlash} store={store} />
|
||||
<Page {...props} query={...getQueryParams()} path={pathWithoutLeadingSlash} store={store} />
|
||||
</div>
|
||||
</DefaultPageLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
import { AppPluginMeta, KeyValue } from '@grafana/data';
|
||||
|
||||
import { RootStore } from 'state/index';
|
||||
|
||||
export interface WithStoreProps {
|
||||
store: RootStore;
|
||||
}
|
||||
|
||||
export interface PageProps<T extends KeyValue = KeyValue> {
|
||||
meta: AppPluginMeta<T>;
|
||||
query: KeyValue;
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
value: string | number;
|
||||
display_name: string;
|
||||
|
|
|
|||
|
|
@ -39,3 +39,7 @@
|
|||
.navbarRootFallback {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
|
|
|||
43
grafana-plugin/src/utils/LocationHelper.ts
Normal file
43
grafana-plugin/src/utils/LocationHelper.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { KeyValue } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
|
||||
import { getQueryParams } from 'plugin/GrafanaPluginRootPage.helpers';
|
||||
|
||||
class LocationHelper {
|
||||
update(params: KeyValue, method: 'replace' | 'push' | 'partial') {
|
||||
const queryParams = getQueryParams();
|
||||
|
||||
const sortedExistingParams = sort(queryParams);
|
||||
const sortedNewParams = sort(params);
|
||||
|
||||
if (toQueryString(sortedExistingParams) !== toQueryString(sortedNewParams)) {
|
||||
if (method === 'partial') {
|
||||
locationService.partial(params);
|
||||
} else {
|
||||
locationService[method](toQueryString(sortedNewParams));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toQueryString(queryParams: KeyValue) {
|
||||
const urlParams = new URLSearchParams(queryParams);
|
||||
for (const [key, value] of Object.entries(queryParams)) {
|
||||
if (Array.isArray(value)) {
|
||||
urlParams.delete(key);
|
||||
value.forEach((v) => urlParams.append(key, v));
|
||||
}
|
||||
}
|
||||
return urlParams.toString();
|
||||
}
|
||||
|
||||
function sort(object: KeyValue) {
|
||||
return Object.keys(object)
|
||||
.sort()
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = object[key];
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export default new LocationHelper();
|
||||
Loading…
Add table
Reference in a new issue