Move alerts inside PageNav (#1040)

# What this PR does

Fix for #904 - TopNav: Move Alerts inside PluginPage
Fix for #908 - Horizontal menu scrolling fix for tabs

There's also a few styling tweaks to be more in match with Grafana core
styles.
This commit is contained in:
Rares Mardare 2023-01-06 15:36:17 +02:00 committed by GitHub
parent a1e4f72280
commit d8dcb673da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 689 additions and 690 deletions

View file

@ -6,25 +6,34 @@ import Header from 'navbar/Header/Header';
import { pages } from 'pages';
import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
import { useStore } from 'state/useStore';
import { DEFAULT_PAGE } from 'utils/consts';
import { useQueryParams } from 'utils/hooks';
export const PluginPage = (isTopNavbar() ? RealPlugin : PluginPageFallback) as React.ComponentType<PluginPageProps>;
export const PluginPage = (
isTopNavbar() ? RealPlugin : PluginPageFallback
) as React.ComponentType<ExtendedPluginPageProps>;
function RealPlugin(props: PluginPageProps): React.ReactNode {
interface ExtendedPluginPageProps extends PluginPageProps {
renderAlertsFn?: () => React.ReactNode;
}
function RealPlugin(props: ExtendedPluginPageProps): React.ReactNode {
const store = useStore();
const queryParams = useQueryParams();
const page = queryParams.get('page');
const page = queryParams.get('page') || DEFAULT_PAGE;
return (
<RealPluginPage {...props}>
{/* Render alerts at the top */}
{props.renderAlertsFn && props.renderAlertsFn()}
<Header page={page} backendLicense={store.backendLicense} />
<h3 className="page-title">{pages[page].text}</h3>
{pages[page].text && <h3 className="page-title">{pages[page].text}</h3>}
{props.children}
</RealPluginPage>
);
}
function PluginPageFallback(props: PluginPageProps): React.ReactNode {
function PluginPageFallback(props: ExtendedPluginPageProps): React.ReactNode {
return props.children;
}

View file

@ -1,15 +1,32 @@
.root {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.alerts_horizontal {
display: flex;
gap: 10px;
.alerts-container {
display: flex;
flex-direction: column;
margin-bottom: 24px;
gap: 10px;
&:empty {
display: none;
}
}
.alert {
margin: 24px 0;
.navbar-legacy .alerts-container {
padding-top: 10px;
}
.alert {
margin: 0;
}
@media (max-width: 768px) {
.navbar-legacy {
padding-top: 50px;
}
}

View file

@ -1,19 +1,24 @@
import plugin from '../../../package.json'; // eslint-disable-line
import React, { FC, useEffect, useState, useCallback } from 'react';
import { Alert } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import { AppRootProps } from 'types';
import PluginLink from 'components/PluginLink/PluginLink';
import { getIfChatOpsConnected } from 'containers/DefaultPageLayout/helper';
import { pages } from 'pages';
import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
import { AppFeature } from 'state/features';
import { useStore } from 'state/useStore';
import LocationHelper from 'utils/LocationHelper';
import { isUserActionAllowed, UserActions } from 'utils/authorization';
import { GRAFANA_LICENSE_OSS } from 'utils/consts';
import { useForceUpdate } from 'utils/hooks';
import { DEFAULT_PAGE, GRAFANA_LICENSE_OSS } from 'utils/consts';
import { useForceUpdate, useQueryParams } from 'utils/hooks';
import plugin from '../../../package.json'; // eslint-disable-line
import { getItem, setItem } from 'utils/localStorage';
import sanitize from 'utils/sanitize';
@ -33,10 +38,12 @@ enum AlertID {
const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
const { children, query } = props;
const queryParams = useQueryParams();
const [showSlackInstallAlert, setShowSlackInstallAlert] = useState<SlackError | undefined>();
const forceUpdate = useForceUpdate();
const page = queryParams.get('page') || DEFAULT_PAGE;
const handleCloseInstallSlackAlert = useCallback(() => {
setShowSlackInstallAlert(undefined);
@ -68,15 +75,41 @@ const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
const isChatOpsConnected = getIfChatOpsConnected(currentUser);
const isPhoneVerified = currentUser?.cloud_connection_status === 3 || currentUser?.verified_phone_number;
return (
<div className={cx('root')}>
<div className={styles.alerts_horizontal}>
if (isTopNavbar()) {
return renderTopNavbar();
}
return renderLegacyNavbar();
function renderTopNavbar(): JSX.Element {
return (
<PluginPage pageNav={pages[page].getPageNav()} renderAlertsFn={renderAlertsFn}>
<div className={cx('root')}>{children}</div>
</PluginPage>
);
}
function renderLegacyNavbar(): JSX.Element {
return (
<PluginPage>
<div className="page-container u-height-100">
<div className={cx('root', 'navbar-legacy')}>
{renderAlertsFn()}
{children}
</div>
</div>
</PluginPage>
);
}
function renderAlertsFn(): JSX.Element {
return (
<div className={cx('alerts-container')}>
{showSlackInstallAlert && (
<Alert
className={styles.alert}
className={cx('alert')}
onRemove={handleCloseInstallSlackAlert}
severity="warning"
// @ts-ignore
title="Slack integration warning"
>
{getSlackMessage(
@ -88,7 +121,7 @@ const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
)}
{currentTeam?.banner.title != null && !getItem(currentTeam?.banner.title) && (
<Alert
className={styles.alert}
className={cx('alert')}
severity="success"
title={currentTeam.banner.title}
onRemove={getRemoveAlertHandler(currentTeam?.banner.title)}
@ -106,7 +139,7 @@ const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
store.backendVersion !== plugin?.version &&
!getItem(`version_mismatch_${store.backendVersion}_${plugin?.version}`) && (
<Alert
className={styles.alert}
className={cx('alert')}
severity="warning"
title={'Version mismatch!'}
onRemove={getRemoveAlertHandler(`version_mismatch_${store.backendVersion}_${plugin?.version}`)}
@ -137,7 +170,7 @@ const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
) && (
<Alert
onRemove={getRemoveAlertHandler(AlertID.CONNECTIVITY_WARNING)}
className={styles.alert}
className={cx('alert')}
severity="warning"
// @ts-ignore
title="Connectivity Warning"
@ -160,9 +193,8 @@ const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
</Alert>
)}
</div>
{children}
</div>
);
);
}
});
export default DefaultPageLayout;

View file

@ -1,24 +1,17 @@
.teamSelect {
width: 200px;
right: 0;
position: absolute;
padding: 16px 0;
margin-right: 24px;
&--topRight {
right: 14px;
top: 12px;
}
&--topRightIncident {
right: 32px;
top: 36px;
}
}
.teamSelectLabel {
display: flex;
}
.teamSelectText,
.teamSelectLink {
line-height: 1.25;
margin-bottom: 4px;
}
.teamSelectLink {
margin-left: auto;
}

View file

@ -49,13 +49,15 @@ const GrafanaTeamSelect = observer((props: GrafanaTeamSelectProps) => {
};
const content = (
<div className={cx('teamSelect', { 'teamSelect--topRight': isTopNavbar() })}>
<div className={cx('teamSelect')}>
<div className={cx('teamSelectLabel')}>
<Label>
Select Team{' '}
<Tooltip content="The objects on this page are filtered by team and you can only view the objects that belong to your team. Note that filtering within Grafana OnCall is meant for usability, not access management.">
<Icon name="info-circle" size="md" className={cx('teamSelectInfo')}></Icon>
</Tooltip>
<span className={cx('teamSelectText')}>
Select Team{''}
<Tooltip content="The objects on this page are filtered by team and you can only view the objects that belong to your team. Note that filtering within Grafana OnCall is meant for usability, not access management.">
<Icon name="info-circle" size="md" className={cx('teamSelectInfo')}></Icon>
</Tooltip>
</span>
</Label>
<WithPermissionControl userAction={UserActions.TeamsWrite}>
<PluginLink path="/org/teams" className={cx('teamSelectLink')}>

View file

@ -4,24 +4,12 @@
max-width: unset !important;
}
.oncall-header {
padding-top: 0;
padding-bottom: 36px;
}
.scrollbar-view h1:first-child {
margin-bottom: 0 !important;
}
.page-container.page-body {
flex-grow: 1 !important;
[class$='-page-header'] {
display: none;
}
.page-container {
max-width: unset !important;
flex-grow: unset !important;
flex-basis: unset !important;
overflow-x: auto;
}
.page-scrollbar-content > div:first-child {
@ -34,34 +22,6 @@
margin-right: 8px;
}
/* This is for Grafana 8, remove later */
@media (max-width: 1540px) {
.page-header__tabs > ul > li > a > div {
display: none;
}
}
@media (max-width: 1540px) {
.page-header__tabs > div > div > a > div {
display: none;
}
}
@media (max-width: 1300px) {
.sidemenu {
position: fixed !important;
height: 100%;
}
.main-view {
padding-left: 50px;
}
.page-header__tabs li a {
white-space: nowrap;
}
}
.page-header__info-block {
flex-grow: 1; /* Stretch the navigation subtitle panel */
}

View file

@ -2,6 +2,11 @@
margin-right: 4px;
}
.header-topnavbar {
padding-top: 0;
padding-bottom: 36px;
}
.navbar-heading {
padding: 4px;
margin: 0 0 0 8px;
@ -16,3 +21,8 @@
align-items: center;
padding-top: 6px;
}
.navbar-left {
display: flex;
flex-basis: 100%;
}

View file

@ -1,7 +1,6 @@
import React from 'react';
import { Card } from '@grafana/ui';
import classnames from 'classnames';
import cn from 'classnames/bind';
import gitHubStarSVG from 'assets/img/github_star.svg';
@ -16,15 +15,16 @@ const cx = cn.bind(styles);
export default function Header({ page, backendLicense }: { page: string; backendLicense: string }) {
return (
<div className="page-container">
<div className="page-header">
<div className={classnames('page-header__inner', { 'oncall-header': isTopNavbar() })}>
<div className={cx('root')}>
<div className={cx('page-header__inner', { 'header-topnavbar': isTopNavbar() })}>
<div className={cx('navbar-left')}>
<span className="page-header__logo">
<img className="page-header__img" src={logo} alt="Grafana OnCall" />
</span>
<div className="page-header__info-block">{renderHeading()}</div>
</div>
<div className={cx('navbar-right')}>
<GrafanaTeamSelect currentPage={page} />
</div>
</div>

View file

@ -1,3 +1,4 @@
.root {
min-width: 1500px;
overflow-x: auto;
white-space: nowrap;
}

View file

@ -19,16 +19,18 @@ export default function LegacyNavTabsBar({ currentPage }: { currentPage: string
.filter((page) => (page.hideFromTabsFn ? !page.hideFromTabsFn(store) : !page.hideFromTabs));
return (
<TabsBar className={cx('root')}>
{navigationPages.map((page, index) => (
<Tab
key={index}
icon={page.icon as IconName}
label={page.text}
href={page.path}
active={currentPage === page.id}
/>
))}
</TabsBar>
<div className={cx('root')}>
<TabsBar>
{navigationPages.map((page, index) => (
<Tab
key={index}
icon={page.icon as IconName}
label={page.text}
href={page.path}
active={currentPage === page.id}
/>
))}
</TabsBar>
</div>
);
}

View file

@ -1,7 +1,6 @@
import React from 'react';
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';
@ -25,7 +24,6 @@ import EscalationChainForm from 'containers/EscalationChainForm/EscalationChainF
import EscalationChainSteps from 'containers/EscalationChainSteps/EscalationChainSteps';
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
import { pages } from 'pages';
import { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import LocationHelper from 'utils/LocationHelper';
@ -135,90 +133,88 @@ class EscalationChainsPage extends React.Component<EscalationChainsPageProps, Es
const searchResult = escalationChainStore.getSearchResult(escalationChainsFilters.searchTerm);
return (
<PluginPage pageNav={pages['escalations'].getPageNav()}>
<PageErrorHandlingWrapper
errorData={errorData}
objectName="escalation"
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} />
<PageErrorHandlingWrapper
errorData={errorData}
objectName="escalation"
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={UserActions.IntegrationsWrite}>
<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>
{!searchResult || searchResult.length ? (
<div className={cx('escalations')}>
<div className={cx('left-column')}>
<WithPermissionControl userAction={UserActions.IntegrationsWrite}>
) : (
<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={UserActions.EscalationChainsWrite}>
<Button
icon="plus"
variant="primary"
size="lg"
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>
) : (
<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={UserActions.EscalationChainsWrite}>
<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}
</VerticalGroup>
}
/>
)}
</>
)}
</PageErrorHandlingWrapper>
</PluginPage>
</div>
{showCreateEscalationChainModal && (
<EscalationChainForm
escalationChainId={escalationChainIdToCopy}
onHide={() => {
this.setState({
showCreateEscalationChainModal: false,
escalationChainIdToCopy: undefined,
});
}}
onUpdate={this.handleEscalationChainCreate}
/>
)}
</>
)}
</PageErrorHandlingWrapper>
);
}

View file

@ -6,6 +6,10 @@
flex-grow: 1;
}
.block {
padding: 0 0 20px 0;
}
.payload-subtitle {
margin-bottom: var(--title-marginBottom);
}

View file

@ -14,7 +14,6 @@ import {
Modal,
Tooltip,
} from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import moment from 'moment-timezone';
@ -46,7 +45,6 @@ import {
GroupedAlert,
} from 'models/alertgroup/alertgroup.types';
import { ResolutionNoteSourceTypesToDisplayName } from 'models/resolution_note/resolution_note.types';
import { pages } from 'pages';
import { PageProps, WithStoreProps } from 'state/types';
import { useStore } from 'state/useStore';
import { withMobXProviderContext } from 'state/withStore';
@ -127,71 +125,69 @@ 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>
<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>
{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>
<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>
);
}
@ -210,7 +206,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
const showLinkTo = !incident.dependent_alert_groups.length && !incident.root_alert_group && !incident.resolved;
return (
<Block withBackground>
<Block withBackground className={cx('block')}>
<VerticalGroup>
<HorizontalGroup className={cx('title')}>
<PluginLink query={{ page: 'incidents', cursor, start, perpage }}>

View file

@ -1,7 +1,6 @@
import React, { ReactElement, SyntheticEvent } from 'react';
import { Button, Icon, Tooltip, VerticalGroup, LoadingPlaceholder, HorizontalGroup } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
import { get } from 'lodash-es';
import { observer } from 'mobx-react';
@ -20,7 +19,6 @@ import IncidentsFilters from 'containers/IncidentsFilters/IncidentsFilters';
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
import { Alert, Alert as AlertType, AlertAction } from 'models/alertgroup/alertgroup.types';
import { User } from 'models/user/user.types';
import { pages } from 'pages';
import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from 'pages/incident/Incident.helpers';
import { move } from 'state/helpers';
import { PageProps, WithStoreProps } from 'state/types';
@ -101,12 +99,10 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
render() {
return (
<PluginPage pageNav={pages['incidents'].getPageNav()}>
<div className={cx('root')}>
{this.renderIncidentFilters()}
{this.renderTable()}
</div>
</PluginPage>
<div className={cx('root')}>
{this.renderIncidentFilters()}
{this.renderTable()}
</div>
);
}

View file

@ -1,7 +1,6 @@
import React from 'react';
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';
@ -24,7 +23,6 @@ import { IntegrationSettingsTab } from 'containers/IntegrationSettings/Integrati
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
import { AlertReceiveChannel } from 'models/alert_receive_channel';
import { AlertReceiveChannelOption } from 'models/alert_receive_channel/alert_receive_channel.types';
import { pages } from 'pages';
import { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import LocationHelper from 'utils/LocationHelper';
@ -131,121 +129,119 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
const searchResult = alertReceiveChannelStore.getSearchResult();
return (
<PluginPage pageNav={pages['integrations'].getPageNav()}>
<PageErrorHandlingWrapper
errorData={errorData}
objectName="integration"
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} />
<PageErrorHandlingWrapper
errorData={errorData}
objectName="integration"
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={UserActions.IntegrationsWrite}>
<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>
{searchResult?.length ? (
<div className={cx('integrations')}>
<div className={cx('integrationsList')}>
) : 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={UserActions.IntegrationsWrite}>
<Button
icon="plus"
variant="primary"
size="lg"
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>
) : 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={UserActions.IntegrationsWrite}>
<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');
}}
</VerticalGroup>
}
/>
) : (
<LoadingPlaceholder text="Loading..." />
)}
{showCreateIntegrationModal && (
<CreateAlertReceiveChannelContainer
onHide={() => {
this.setState({ showCreateIntegrationModal: false });
}}
onCreate={this.handleCreateNewAlertReceiveChannel}
/>
)}
</>
)}
</PageErrorHandlingWrapper>
</PluginPage>
</div>
{alertReceiveChannelToShowSettings && (
<IntegrationSettings
onUpdate={() => {
alertReceiveChannelStore.updateItem(alertReceiveChannelToShowSettings);
}}
startTab={integrationSettingsTab}
id={alertReceiveChannelToShowSettings}
onHide={() => {
this.setState({
alertReceiveChannelToShowSettings: undefined,
integrationSettingsTab: undefined,
});
LocationHelper.update({ tab: undefined }, 'partial');
}}
/>
)}
{showCreateIntegrationModal && (
<CreateAlertReceiveChannelContainer
onHide={() => {
this.setState({ showCreateIntegrationModal: false });
}}
onCreate={this.handleCreateNewAlertReceiveChannel}
/>
)}
</>
)}
</PageErrorHandlingWrapper>
);
}

View file

@ -1,7 +1,6 @@
import React from 'react';
import { Button, VerticalGroup } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import moment from 'moment-timezone';
@ -16,7 +15,6 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm
import { getAlertReceiveChannelDisplayName } from 'models/alert_receive_channel/alert_receive_channel.helpers';
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 { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import { UserActions } from 'utils/authorization';
@ -117,7 +115,7 @@ class MaintenancePage extends React.Component<MaintenancePageProps, MaintenanceP
];
return (
<PluginPage pageNav={pages['maintenance'].getPageNav()}>
<>
<div className={cx('root')}>
<GTable
emptyText={data ? 'No maintenances found' : 'Loading...'}
@ -160,7 +158,7 @@ class MaintenancePage extends React.Component<MaintenancePageProps, MaintenanceP
}}
/>
)}
</PluginPage>
</>
);
}

View file

@ -1,7 +1,6 @@
import React from 'react';
import { Button, HorizontalGroup, Tag, Tooltip } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
import { debounce } from 'lodash-es';
import { observer } from 'mobx-react';
@ -96,41 +95,39 @@ class OrganizationLogPage extends React.Component<OrganizationLogProps, Organiza
const loading = !results;
return (
<PluginPage>
<div className={cx('root')}>
<OrganizationLogFilters value={filters} onChange={this.handleChangeOrganizationLogFilters} />
<GTable
rowKey="id"
title={() => (
<div className={cx('header')}>
<Text.Title className={cx('users-title')} level={3}>
Organization Logs
</Text.Title>
<Button onClick={this.refresh} icon={loading ? 'fa fa-spinner' : 'sync'}>
Refresh
</Button>
</div>
)}
showHeader={true}
data={results}
loading={loading}
emptyText={results ? 'No logs found' : 'Loading...'}
columns={columns}
pagination={{
page,
total: Math.ceil((total || 0) / ITEMS_PER_PAGE),
onChange: this.handleChangePage,
}}
rowClassName={cx('align-top')}
expandable={{
expandedRowRender: this.renderFullDescription,
expandRowByClick: true,
expandedRowKeys: expandedLogsKeys,
onExpandedRowsChange: this.handleExpandedRowsChange,
}}
/>
</div>
</PluginPage>
<div className={cx('root')}>
<OrganizationLogFilters value={filters} onChange={this.handleChangeOrganizationLogFilters} />
<GTable
rowKey="id"
title={() => (
<div className={cx('header')}>
<Text.Title className={cx('users-title')} level={3}>
Organization Logs
</Text.Title>
<Button onClick={this.refresh} icon={loading ? 'fa fa-spinner' : 'sync'}>
Refresh
</Button>
</div>
)}
showHeader={true}
data={results}
loading={loading}
emptyText={results ? 'No logs found' : 'Loading...'}
columns={columns}
pagination={{
page,
total: Math.ceil((total || 0) / ITEMS_PER_PAGE),
onChange: this.handleChangePage,
}}
rowClassName={cx('align-top')}
expandable={{
expandedRowRender: this.renderFullDescription,
expandRowByClick: true,
expandedRowKeys: expandedLogsKeys,
onExpandedRowsChange: this.handleExpandedRowsChange,
}}
/>
</div>
);
}

View file

@ -1,7 +1,6 @@
import React from 'react';
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';
@ -19,7 +18,6 @@ import OutgoingWebhookForm from 'containers/OutgoingWebhookForm/OutgoingWebhookF
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
import { ActionDTO } from 'models/action';
import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types';
import { pages } from 'pages';
import { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import LocationHelper from 'utils/LocationHelper';
@ -111,54 +109,52 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
];
return (
<PluginPage pageNav={pages['outgoing_webhooks'].getPageNav()}>
<PageErrorHandlingWrapper
errorData={errorData}
objectName="outgoing webhook"
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={!isUserActionAllowed(UserActions.OutgoingWebhooksWrite)}
>
<WithPermissionControl userAction={UserActions.OutgoingWebhooksWrite}>
<Button variant="primary" icon="plus">
Create
</Button>
</WithPermissionControl>
</PluginLink>
</div>
<PageErrorHandlingWrapper
errorData={errorData}
objectName="outgoing webhook"
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={!isUserActionAllowed(UserActions.OutgoingWebhooksWrite)}
>
<WithPermissionControl userAction={UserActions.OutgoingWebhooksWrite}>
<Button variant="primary" icon="plus">
Create
</Button>
</WithPermissionControl>
</PluginLink>
</div>
)}
rowKey="id"
columns={columns}
data={webhooks}
/>
</div>
{outgoingWebhookIdToEdit && (
<OutgoingWebhookForm
id={outgoingWebhookIdToEdit}
onUpdate={this.update}
onHide={this.handleOutgoingWebhookFormHide}
/>
)}
</>
)}
</PageErrorHandlingWrapper>
</PluginPage>
</div>
)}
rowKey="id"
columns={columns}
data={webhooks}
/>
</div>
{outgoingWebhookIdToEdit && (
<OutgoingWebhookForm
id={outgoingWebhookIdToEdit}
onUpdate={this.update}
onHide={this.handleOutgoingWebhookFormHide}
/>
)}
</>
)}
</PageErrorHandlingWrapper>
);
}

View file

@ -1,7 +1,6 @@
import React from 'react';
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';
@ -20,7 +19,6 @@ import ScheduleICalSettings from 'containers/ScheduleIcalLink/ScheduleIcalLink';
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 { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import LocationHelper from 'utils/LocationHelper';
@ -112,156 +110,154 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
shiftIdToShowOverridesForm;
return (
<PluginPage pageNav={pages['schedule'].getPageNav()}>
<PageErrorHandlingWrapper pageName="schedules">
{() => (
<>
<div className={cx('root')}>
<VerticalGroup spacing="lg">
<div className={cx('header')}>
<HorizontalGroup justify="space-between">
<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 && (
<HorizontalGroup>
<Text type="secondary">Current timezone:</Text>
<UserTimezoneSelect
value={currentTimezone}
users={users}
onChange={this.handleTimezoneChange}
/>
</HorizontalGroup>
)}
<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 && (
<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>
{(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>
</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 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>
</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>
</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>
</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>
)}
</>
)}
</PageErrorHandlingWrapper>
</PluginPage>
<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>
{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>
);
}

View file

@ -1,7 +1,6 @@
import React from 'react';
import { Button, HorizontalGroup, IconButton, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { debounce } from 'lodash-es';
@ -25,7 +24,6 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm
import { Schedule, ScheduleType } from 'models/schedule/schedule.types';
import { getSlackChannelName } from 'models/slack_channel/slack_channel.helpers';
import { Timezone } from 'models/timezone/timezone.types';
import { pages } from 'pages';
import { getStartOfWeek } from 'pages/schedule/Schedule.helpers';
import { WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
@ -135,7 +133,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
: undefined;
return (
<PluginPage pageNav={pages['schedules'].getPageNav()}>
<>
<div className={cx('root')}>
<VerticalGroup>
<HorizontalGroup justify="space-between">
@ -192,7 +190,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
}}
/>
)}
</PluginPage>
</>
);
}

View file

@ -1,7 +1,6 @@
import React from 'react';
import { Tab, TabsBar } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
@ -35,11 +34,7 @@ class SettingsPage extends React.Component<SettingsPageProps, SettingsPageState>
};
render() {
return (
<PluginPage pageNav={this.getMatchingPageNav()}>
<div className={cx('root')}>{this.renderContent()}</div>
</PluginPage>
);
return <div className={cx('root')}>{this.renderContent()}</div>;
}
renderContent() {

View file

@ -1,7 +1,6 @@
import React from 'react';
import { Button } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
@ -17,13 +16,11 @@ const cx = cn.bind(styles);
class Test extends React.Component<any, any> {
render() {
return (
<PluginPage>
<div className={cx('root')}>
<WithPermissionControl userAction={UserActions.SchedulesWrite}>
{(disabled) => <Button disabled={disabled}>Click me!</Button>}
</WithPermissionControl>
</div>
</PluginPage>
<div className={cx('root')}>
<WithPermissionControl userAction={UserActions.SchedulesWrite}>
{(disabled) => <Button disabled={disabled}>Click me!</Button>}
</WithPermissionControl>
</div>
);
}
}

View file

@ -1,7 +1,6 @@
import React from 'react';
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';
@ -20,7 +19,6 @@ import UsersFilters from 'components/UsersFilters/UsersFilters';
import UserSettings from 'containers/UserSettings/UserSettings';
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
import { User as UserType } from 'models/user/user.types';
import { pages } from 'pages';
import { PageProps, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import LocationHelper from 'utils/LocationHelper';
@ -159,85 +157,83 @@ class Users extends React.Component<UsersProps, UsersState> {
const { count, results } = userStore.getSearchResult();
return (
<PluginPage pageNav={pages['users'].getPageNav()}>
<PageErrorHandlingWrapper
errorData={errorData}
objectName="user"
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>
<PageErrorHandlingWrapper
errorData={errorData}
objectName="user"
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>
<PluginLink partial query={{ id: 'me' }}>
<Button variant="primary" icon="user">
View my profile
</Button>
</PluginLink>
</div>
{isUserActionAllowed(UserActions.UserSettingsRead) ? (
<>
<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,
}}
/>
</>
) : (
<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"
/>
)}
<PluginLink partial query={{ id: 'me' }}>
<Button variant="primary" icon="user">
View my profile
</Button>
</PluginLink>
</div>
{userPkToEdit && <UserSettings id={userPkToEdit} onHide={this.handleHideUserSettings} />}
{isUserActionAllowed(UserActions.UserSettingsRead) ? (
<>
<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,
}}
/>
</>
) : (
<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>
</>
)}
</PageErrorHandlingWrapper>
</PluginPage>
{userPkToEdit && <UserSettings id={userPkToEdit} onHide={this.handleHideUserSettings} />}
</div>
</>
)}
</PageErrorHandlingWrapper>
);
}

View file

@ -23,6 +23,7 @@ import { routes } from 'pages/routes';
import { rootStore } from 'state';
import { useStore } from 'state/useStore';
import { isUserActionAllowed } from 'utils/authorization';
import { DEFAULT_PAGE } from 'utils/consts';
import { useQueryParams, useQueryPath } from 'utils/hooks';
dayjs.extend(utc);
@ -49,7 +50,7 @@ export const GrafanaPluginRootPage = (props: AppRootProps) => (
export const Root = observer((props: AppRootProps) => {
const [didFinishLoading, setDidFinishLoading] = useState(false);
const queryParams = useQueryParams();
const page = queryParams.get('page');
const page = queryParams.get('page') || DEFAULT_PAGE;
const path = useQueryPath();
// Required to support grafana instances that use a custom `root_url`.
@ -93,18 +94,15 @@ export const Root = observer((props: AppRootProps) => {
{!isTopNavbar() && (
<>
<Header page={page} backendLicense={store.backendLicense} />
<nav className="page-container">
<LegacyNavTabsBar currentPage={page} />
</nav>
<LegacyNavTabsBar currentPage={page} />
</>
)}
<div
className={classnames(
{ 'page-container': !isTopNavbar() },
{ 'page-body': !isTopNavbar() },
'u-position-relative'
)}
className={classnames('u-position-relative', 'u-flex-grow-1', {
'u-overflow-x-auto': !isTopNavbar(),
'page-body': !isTopNavbar(),
})}
>
{userHasAccess ? (
<Page {...props} query={...getQueryParams()} path={pathWithoutLeadingSlash} store={store} />

View file

@ -2,6 +2,10 @@
position: relative;
}
.u-overflow-x-auto {
overflow-x: auto;
}
.u-pull-right {
margin-left: auto;
}
@ -18,6 +22,10 @@
width: 100%;
}
.u-height-100 {
height: 100%;
}
.u-flex {
display: flex;
flex-direction: row;
@ -28,6 +36,10 @@
align-items: center;
}
.u-flex-grow-1 {
flex-grow: 1;
}
.u-align-items-center {
align-items: center;
}

View file

@ -7,3 +7,5 @@ export const GRAFANA_LICENSE_OSS = 'OpenSource';
// Reusable breakpoint sizes
export const BREAKPOINT_TABS = 1024;
export const DEFAULT_PAGE = 'incidents';