More emotionjs migration changes (#4362)
# What this PR does - Get rid of some of the hardcoded css variables defined in `vars.css`, specifically tags-related, but not only - Migrated a few more components to emotion
This commit is contained in:
parent
d52e821c33
commit
f7beced64e
68 changed files with 1674 additions and 1449 deletions
|
|
@ -7,12 +7,6 @@
|
|||
--always-gray: #ccccdc;
|
||||
--title-marginBottom: 16px;
|
||||
--opacity: 0.5;
|
||||
--tag-danger: #e02f44;
|
||||
--tag-warning: #c69b06;
|
||||
--tag-primary: #299c46;
|
||||
--tag-secondary: #464c54;
|
||||
--tag-secondary-transparent: rgba(204, 204, 220, 0.07);
|
||||
--tag-border-link: rgba(56, 113, 220, 0.2);
|
||||
}
|
||||
|
||||
.theme-light {
|
||||
|
|
@ -32,25 +26,12 @@
|
|||
--oncall-icon-stroke-color: #fff;
|
||||
--hover-selected: #f4f5f5;
|
||||
--background-canvas: #f4f5f5;
|
||||
--background-primary: #fff;
|
||||
--background-secondary: #f4f5f5;
|
||||
--border-medium-color: rgba(36, 41, 46, 0.3);
|
||||
--border-medium: 1px solid rgba(36, 41, 46, 0.3);
|
||||
--border-strong: 1px solid rgba(36, 41, 46, 0.4);
|
||||
--border-weak: 1px solid rgba(36, 41, 46, 0.12);
|
||||
--shadows-z3: 0 13px 20px 1px rgba(24, 26, 27, 0.18);
|
||||
--tag-background-primary: rgba(50, 116, 217, 0.15);
|
||||
--tag-border-primary: rgb(136, 174, 233);
|
||||
--tag-text-primary: rgb(26, 71, 139);
|
||||
--tag-background-warning: rgba(255, 120, 10, 0.15);
|
||||
--tag-border-warning: rgb(255, 176, 112);
|
||||
--tag-text-warning: rgb(163, 73, 0);
|
||||
--tag-background-success: rgba(86, 166, 75, 0.15);
|
||||
--tag-border-success: rgb(148, 203, 140);
|
||||
--tag-text-success: rgb(50, 96, 43);
|
||||
--tag-background-danger: rgba(224, 47, 68, 0.15);
|
||||
--tag-border-danger: rgb(237, 136, 148);
|
||||
--tag-text-danger: rgb(147, 22, 37);
|
||||
--button-background: rgba(36, 41, 46, 0.08);
|
||||
--button-hover-background: rgba(36, 41, 46, 0.15);
|
||||
--box-background: rgba(244, 245, 245);
|
||||
|
|
@ -79,25 +60,12 @@
|
|||
--hover-selected-hardcoded: #34363d;
|
||||
--oncall-icon-stroke-color: #181b1f;
|
||||
--background-canvas: #111217;
|
||||
--background-primary: #181b1f;
|
||||
--background-secondary: #22252b;
|
||||
--border-medium-color: rgba(204, 204, 220, 0.15);
|
||||
--border-medium: 1px solid rgba(204, 204, 220, 0.15);
|
||||
--border-strong: 1px solid rgba(204, 204, 220, 0.25);
|
||||
--border-weak: 1px solid rgba(204, 204, 220, 0.07);
|
||||
--shadows-z3: 0 8px 24px rgb(1, 4, 9);
|
||||
--tag-background-primary: rgba(87, 148, 242, 0.15);
|
||||
--tag-border-primary: rgb(13, 72, 163);
|
||||
--tag-text-primary: rgb(158, 193, 247);
|
||||
--tag-background-warning: rgba(255, 152, 48, 0.15);
|
||||
--tag-border-warning: rgb(150, 75, 0);
|
||||
--tag-text-warning: rgb(255, 190, 124);
|
||||
--tag-background-success: rgba(115, 191, 105, 0.15);
|
||||
--tag-border-success: rgb(49, 100, 43);
|
||||
--tag-text-success: rgb(165, 214, 159);
|
||||
--tag-background-danger: rgba(242, 73, 92, 0.15);
|
||||
--tag-border-danger: rgb(151, 11, 27);
|
||||
--tag-text-danger: rgb(247, 144, 156);
|
||||
--box-background: rgba(10, 10, 10, 0.4);
|
||||
--working-hours-shades-color: rgba(17, 18, 23, 0.15);
|
||||
--working-hours-shades-color-light: rgba(17, 18, 23, 0.1);
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ export const CardButton: FC<CardButtonProps> = (props) => {
|
|||
className={cx(styles.root, { [styles.rootSelected]: selected })}
|
||||
data-testid="test__cardButton"
|
||||
>
|
||||
<div className={cx(styles.icon)}>{icon}</div>
|
||||
<div className={cx(styles.meta)}>
|
||||
<div className={styles.icon}>{icon}</div>
|
||||
<div className={styles.meta}>
|
||||
<VerticalGroup spacing="xs">
|
||||
<Text type="secondary">{description}</Text>
|
||||
<Text.Title level={1}>{title}</Text.Title>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export const Collapse: FC<CollapseProps> = (props) => {
|
|||
data-testid="test__toggle"
|
||||
>
|
||||
<Icon name={'angle-right'} size="xl" className={cx(styles.icon, { [bem(styles.icon, 'rotated')]: isOpen })} />
|
||||
<div className={cx(styles.label)}> {label}</div>
|
||||
<div className={styles.label}> {label}</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className={cx(styles.content, contentClassName)} data-testid="test__children">
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export const GList = <T extends WithId>(props: GListProps<T>) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={cx(styles.root)}>
|
||||
<div className={styles.root}>
|
||||
{items ? (
|
||||
items.map((item) => (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export const GTable = <RT extends DefaultRecordType = DefaultRecordType>(props:
|
|||
key: 'check',
|
||||
title: (
|
||||
<Checkbox
|
||||
className={cx(styles.checkbox)}
|
||||
className={styles.checkbox}
|
||||
onChange={handleMasterCheckboxChange}
|
||||
value={data?.length > 0 && rowSelection.selectedRowKeys.length === data?.length}
|
||||
/>
|
||||
|
|
@ -124,7 +124,7 @@ export const GTable = <RT extends DefaultRecordType = DefaultRecordType>(props:
|
|||
render: (item: any) => {
|
||||
return (
|
||||
<Checkbox
|
||||
className={cx(styles.checkbox)}
|
||||
className={styles.checkbox}
|
||||
value={rowSelection.selectedRowKeys.includes(item[rowKey as string])}
|
||||
onChange={getCheckboxClickHandler(item[rowKey as string])}
|
||||
/>
|
||||
|
|
@ -136,7 +136,7 @@ export const GTable = <RT extends DefaultRecordType = DefaultRecordType>(props:
|
|||
}, [rowSelection, columnsProp, data]);
|
||||
|
||||
return (
|
||||
<div className={cx(styles.root)} data-testid="test__gTable">
|
||||
<div className={styles.root} data-testid="test__gTable">
|
||||
<Table<RT>
|
||||
expandable={expandable}
|
||||
rowKey={rowKey}
|
||||
|
|
@ -148,7 +148,7 @@ export const GTable = <RT extends DefaultRecordType = DefaultRecordType>(props:
|
|||
{...restProps}
|
||||
/>
|
||||
{pagination && (
|
||||
<div className={cx(styles.pagination)}>
|
||||
<div className={styles.pagination}>
|
||||
<Pagination hideWhenSinglePage currentPage={page} numberOfPages={numberOfPages} onNavigate={onNavigate} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ const IntegrationCollapsibleTreeItem: React.FC<{
|
|||
return (
|
||||
<div className={cx(styles.group, { [bem(styles.group, 'hidden')]: item.isHidden }, 'group')} data-emotion="group">
|
||||
<div
|
||||
className={cx(styles.icon)}
|
||||
className={styles.icon}
|
||||
style={{
|
||||
transform: `translateY(${item.startingElemPosition || 0})`,
|
||||
}}
|
||||
|
|
@ -139,7 +139,7 @@ const IntegrationCollapsibleTreeItem: React.FC<{
|
|||
function renderIcon() {
|
||||
if (item.isTextIcon && elementPosition) {
|
||||
return (
|
||||
<Text type="primary" customTag="h6" className={cx(styles.numberIcon)}>
|
||||
<Text type="primary" customTag="h6" className={styles.numberIcon}>
|
||||
{elementPosition}
|
||||
</Text>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export const IntegrationInputField: React.FC<IntegrationInputFieldProps> = ({
|
|||
|
||||
return (
|
||||
<div className={cx(styles.root, { [className]: !!className })}>
|
||||
<div className={cx(styles.inputContainer)}>{renderInputField()}</div>
|
||||
<div className={styles.inputContainer}>{renderInputField()}</div>
|
||||
|
||||
<div className={cx(styles.icons, iconsClassName)}>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export const IntegrationBlock: React.FC<IntegrationBlockProps> = ({
|
|||
</Block>
|
||||
)}
|
||||
{content && (
|
||||
<div className={cx(styles.integrationBlockContent)} onClick={toggle}>
|
||||
<div className={styles.integrationBlockContent} onClick={toggle}>
|
||||
{content}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
|
|
@ -12,8 +12,8 @@ export const IntegrationBlockItem: React.FC<IntegrationBlockItemProps> = (props)
|
|||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={cx(styles.parent)} data-testid="integration-block-item">
|
||||
<div className={cx(styles.content)}>{props.children}</div>
|
||||
<div className={styles.parent} data-testid="integration-block-item">
|
||||
<div className={styles.content}>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, InlineLabel, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
|
||||
|
||||
|
|
@ -41,11 +41,11 @@ export const IntegrationTemplateBlock: React.FC<IntegrationTemplateBlockProps> =
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={cx(styles.container)}>
|
||||
<div className={styles.container}>
|
||||
<InlineLabel width={20} {...inlineLabelProps}>
|
||||
{label}
|
||||
</InlineLabel>
|
||||
<div className={cx(styles.item)}>
|
||||
<div className={styles.item}>
|
||||
{renderInput()}
|
||||
{isTemplateEditable && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC, useCallback, useState } from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import { Button, Drawer, HorizontalGroup, Icon, VerticalGroup, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { Block } from 'components/GBlock/Block';
|
||||
|
|
@ -33,9 +33,9 @@ export const NewScheduleSelector: FC<NewScheduleSelectorProps> = ({ onHide, onCr
|
|||
|
||||
return (
|
||||
<Drawer scrollableContent title="Create new schedule" onClose={onHide} closeOnMaskClick={false}>
|
||||
<div className={cx(styles.content)}>
|
||||
<div className={styles.content}>
|
||||
<VerticalGroup spacing="lg">
|
||||
<Block bordered withBackground className={cx(styles.block)}>
|
||||
<Block bordered withBackground className={styles.block}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="md">
|
||||
<Icon name="calendar-alt" size="xl" />
|
||||
|
|
@ -53,7 +53,7 @@ export const NewScheduleSelector: FC<NewScheduleSelectorProps> = ({ onHide, onCr
|
|||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</Block>
|
||||
<Block bordered withBackground className={cx(styles.block)}>
|
||||
<Block bordered withBackground className={styles.block}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="md">
|
||||
<Icon name="download-alt" size="xl" />
|
||||
|
|
@ -69,7 +69,7 @@ export const NewScheduleSelector: FC<NewScheduleSelectorProps> = ({ onHide, onCr
|
|||
</Button>
|
||||
</HorizontalGroup>
|
||||
</Block>
|
||||
<Block bordered withBackground className={cx(styles.block)}>
|
||||
<Block bordered withBackground className={styles.block}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="md">
|
||||
<Icon name="cog" size="xl" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect } from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { VerticalGroup, useStyles2 } from '@grafana/ui';
|
||||
|
||||
|
|
@ -52,9 +52,9 @@ export const PageErrorHandlingWrapper = function ({
|
|||
const { wrongTeamNoPermissions } = errorData;
|
||||
|
||||
return (
|
||||
<div className={cx(styles.notFound)}>
|
||||
<div className={styles.notFound}>
|
||||
<VerticalGroup spacing="lg" align="center">
|
||||
<Text.Title level={1} className={cx(styles.errorCode)}>
|
||||
<Text.Title level={1} className={styles.errorCode}>
|
||||
403
|
||||
</Text.Title>
|
||||
{wrongTeamNoPermissions && (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
interface ScheduleBorderedAvatarProps {
|
||||
|
|
@ -20,13 +20,13 @@ export const ScheduleBorderedAvatar = function ({
|
|||
}: ScheduleBorderedAvatarProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return <div className={cx(styles.root)}>{renderSVG()}</div>;
|
||||
return <div className={styles.root}>{renderSVG()}</div>;
|
||||
|
||||
function renderAvatarIcon() {
|
||||
return (
|
||||
<>
|
||||
<div className={cx(styles.avatar)}>{renderAvatar()}</div>
|
||||
<div className={cx(styles.icon)}>{renderIcon()}</div>
|
||||
<div className={styles.avatar}>{renderAvatar()}</div>
|
||||
<div className={styles.icon}>{renderIcon()}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { InlineSwitch, useStyles2 } from '@grafana/ui';
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ export const ScheduleFilters = (props: SchedulesFiltersProps) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={cx(styles.root)}>
|
||||
<div className={styles.root}>
|
||||
<InlineSwitch
|
||||
showLabel
|
||||
label="Highlight my shifts"
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export const ScheduleQuality: FC<ScheduleQualityProps> = observer(({ schedule })
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className={cx(styles.root)} data-testid="schedule-quality">
|
||||
<div className={styles.root} data-testid="schedule-quality">
|
||||
{relatedScheduleEscalationChains?.length > 0 && schedule?.number_of_escalation_chains > 0 && (
|
||||
<TooltipBadge
|
||||
borderType="success"
|
||||
|
|
@ -88,7 +88,7 @@ export const ScheduleQuality: FC<ScheduleQualityProps> = observer(({ schedule })
|
|||
content={<ScheduleQualityDetails quality={quality} getScheduleQualityString={getScheduleQualityString} />}
|
||||
>
|
||||
<div className={cx(utils.cursorDefault)}>
|
||||
<Tag className={cx(styles.tag)} color={getTagSeverity()}>
|
||||
<Tag className={styles.tag} color={getTagSeverity()}>
|
||||
Quality: <strong>{getScheduleQualityString(quality.total_score)}</strong>
|
||||
</Tag>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,12 +30,12 @@ export const ScheduleQualityDetails: FC<ScheduleQualityDetailsProps> = ({ qualit
|
|||
const warningComments = comments.filter((c) => c.type === 'warning');
|
||||
|
||||
return (
|
||||
<div className={cx(styles.root)} data-testid="schedule-quality-details">
|
||||
<div className={cx(styles.container)}>
|
||||
<div className={styles.root} data-testid="schedule-quality-details">
|
||||
<div className={styles.container}>
|
||||
<div className={cx(styles.container, bem(styles.container, 'withLateralPadding'))}>
|
||||
<Text type="secondary" className={cx(styles.header)}>
|
||||
<Text type="secondary" className={styles.header}>
|
||||
Schedule quality:{' '}
|
||||
<Text type="primary" className={cx(styles.headerSubText)}>
|
||||
<Text type="primary" className={styles.headerSubText}>
|
||||
{getScheduleQualityString(score)}
|
||||
</Text>
|
||||
</Text>
|
||||
|
|
@ -53,10 +53,10 @@ export const ScheduleQualityDetails: FC<ScheduleQualityDetailsProps> = ({ qualit
|
|||
<>
|
||||
{/* Show Info comments */}
|
||||
{infoComments?.length > 0 && (
|
||||
<div className={cx(styles.container)}>
|
||||
<div className={cx(styles.row)}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.row}>
|
||||
<Icon name="info-circle" />
|
||||
<div className={cx(styles.container)}>
|
||||
<div className={styles.container}>
|
||||
{infoComments.map((comment, index) => (
|
||||
<Text type="primary" key={index}>
|
||||
{comment.text}
|
||||
|
|
@ -69,10 +69,10 @@ export const ScheduleQualityDetails: FC<ScheduleQualityDetailsProps> = ({ qualit
|
|||
|
||||
{/* Show Warning comments afterwards */}
|
||||
{warningComments?.length > 0 && (
|
||||
<div className={cx(styles.container)}>
|
||||
<div className={cx(styles.row)}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.row}>
|
||||
<Icon name="calendar-alt" />
|
||||
<div className={cx(styles.container)}>
|
||||
<div className={styles.container}>
|
||||
<Text type="secondary">Rotation structure issues</Text>
|
||||
{warningComments.map((comment, index) => (
|
||||
<Text type="primary" key={index}>
|
||||
|
|
@ -87,13 +87,13 @@ export const ScheduleQualityDetails: FC<ScheduleQualityDetailsProps> = ({ qualit
|
|||
)}
|
||||
|
||||
{overloaded_users?.length > 0 && (
|
||||
<div className={cx(styles.container)}>
|
||||
<div className={cx(styles.row)}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.row}>
|
||||
<Icon name="users-alt" />
|
||||
<div className={cx(styles.container)}>
|
||||
<div className={styles.container}>
|
||||
<Text type="secondary">Overloaded users</Text>
|
||||
{overloaded_users.map((overloadedUser, index) => (
|
||||
<Text type="primary" className={cx(styles.username)} key={index}>
|
||||
<Text type="primary" className={styles.username} key={index}>
|
||||
{overloadedUser.username} (+{overloadedUser.score}% avg)
|
||||
</Text>
|
||||
))}
|
||||
|
|
@ -115,7 +115,7 @@ export const ScheduleQualityDetails: FC<ScheduleQualityDetailsProps> = ({ qualit
|
|||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="sm">
|
||||
<Icon name="calculator-alt" />
|
||||
<Text type="secondary" className={cx(styles.metholodogy)}>
|
||||
<Text type="secondary" className={styles.metholodogy}>
|
||||
Calculation methodology
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
|
|
@ -126,7 +126,7 @@ export const ScheduleQualityDetails: FC<ScheduleQualityDetailsProps> = ({ qualit
|
|||
/>
|
||||
</HorizontalGroup>
|
||||
{expanded && (
|
||||
<Text type="primary" className={cx(styles.text)}>
|
||||
<Text type="primary" className={styles.text}>
|
||||
The next 52 weeks (~1 year) are taken into account when generating the quality report. Refer to the{' '}
|
||||
<a
|
||||
href={'https://grafana.com/docs/oncall/latest/on-call-schedules/web-schedule/#schedule-quality-report'}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const ScheduleQualityProgressBar: React.FC<ProgressBarProps> = ({ classNa
|
|||
const classList = [styles.bar, className || ''];
|
||||
|
||||
return (
|
||||
<div className={cx(styles.wrapper)}>
|
||||
<div className={styles.wrapper}>
|
||||
{!numTotalSteps && <div className={classList.join(' ')} style={{ width: `${completed}%` }} />}
|
||||
{renderSteps(numTotalSteps, completed)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export const SourceCode: FC<SourceCodeProps> = ({
|
|||
{showClipboardIconOnly ? (
|
||||
<IconButton
|
||||
aria-label="Copy"
|
||||
className={cx(styles.copyIcon)}
|
||||
className={styles.copyIcon}
|
||||
size={'lg'}
|
||||
name="copy"
|
||||
data-emotion="copyIcon"
|
||||
|
|
@ -62,7 +62,7 @@ export const SourceCode: FC<SourceCodeProps> = ({
|
|||
/>
|
||||
) : (
|
||||
<Button
|
||||
className={cx(styles.copyIcon)}
|
||||
className={styles.copyIcon}
|
||||
variant="primary"
|
||||
size="xs"
|
||||
icon="copy"
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export const GTable: FC<Props> = (props) => {
|
|||
</div>
|
||||
);
|
||||
},
|
||||
expandedRowClassName: (_record, index) => (index % 2 === 0 ? cx(styles.rowEven) : ''),
|
||||
expandedRowClassName: (_record, index) => (index % 2 === 0 ? styles.rowEven : ''),
|
||||
}
|
||||
: null;
|
||||
}, [expandable]);
|
||||
|
|
@ -61,11 +61,11 @@ export const GTable: FC<Props> = (props) => {
|
|||
columns={columns}
|
||||
data={data}
|
||||
expandable={expandableFn}
|
||||
rowClassName={(_record, index) => (index % 2 === 0 ? cx(styles.rowEven) : '')}
|
||||
rowClassName={(_record, index) => (index % 2 === 0 ? styles.rowEven : '')}
|
||||
{...restProps}
|
||||
/>
|
||||
{pagination && (
|
||||
<div className={cx(styles.pagination)}>
|
||||
<div className={styles.pagination}>
|
||||
<Pagination hideWhenSinglePage currentPage={page} numberOfPages={numberOfPages} onNavigate={onNavigate} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { FC } from 'react';
|
|||
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { bem, getLabelCss } from 'styles/utils.styles';
|
||||
|
||||
interface TagProps {
|
||||
|
|
@ -30,6 +30,7 @@ export enum TagColor {
|
|||
|
||||
export const Tag: FC<TagProps> = (props) => {
|
||||
const { color, children, className, onClick, size = 'medium' } = props;
|
||||
const theme = useTheme2();
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ export const Tag: FC<TagProps> = (props) => {
|
|||
styles[color]
|
||||
: css`
|
||||
background-color: ${color};
|
||||
color: text;
|
||||
color: ${theme.colors.primary.contrastText};
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export const TooltipBadge: FC<TooltipBadgeProps> = (props) => {
|
|||
placement={placement || 'bottom-start'}
|
||||
interactive
|
||||
content={
|
||||
<div className={cx(styles.tooltip)}>
|
||||
<div className={styles.tooltip}>
|
||||
<VerticalGroup spacing="xs">
|
||||
<Text type="primary">{tooltipTitle}</Text>
|
||||
{tooltipContent && <Text type="secondary">{tooltipContent}</Text>}
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ export const Tutorial: FC<TutorialProps> = (props) => {
|
|||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<Block className={cx(styles.root)} bordered>
|
||||
<div className={cx(styles.title)}>{title}</div>
|
||||
<div className={cx(styles.steps)}>
|
||||
<div className={cx(styles.step)}>
|
||||
<Block className={styles.root} bordered>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.steps}>
|
||||
<div className={styles.step}>
|
||||
<PluginLink query={{ page: 'integrations' }}>
|
||||
<div className={cx(styles.icon, { [bem(styles.icon, 'active')]: step === TutorialStep.Integrations })}>
|
||||
<img src={integrationsIcon} />
|
||||
|
|
@ -38,7 +38,7 @@ export const Tutorial: FC<TutorialProps> = (props) => {
|
|||
<Text type="secondary">Add integration with a monitoring system</Text>
|
||||
</div>
|
||||
<Arrow />
|
||||
<div className={cx(styles.step)}>
|
||||
<div className={styles.step}>
|
||||
<PluginLink query={{ page: 'escalations' }}>
|
||||
<div className={cx(styles.icon, { [bem(styles.icon, 'active')]: step === TutorialStep.Escalations })}>
|
||||
<img src={escalationIcon} />
|
||||
|
|
@ -47,7 +47,7 @@ export const Tutorial: FC<TutorialProps> = (props) => {
|
|||
<Text type="secondary">Setup escalation chain to handle notifications</Text>
|
||||
</div>
|
||||
<Arrow />
|
||||
<div className={cx(styles.step)}>
|
||||
<div className={styles.step}>
|
||||
<PluginLink query={{ page: 'chat-ops' }}>
|
||||
<div className={cx(styles.icon, { [bem(styles.icon, 'active')]: step === TutorialStep.Slack })}>
|
||||
<img src={chatIcon} />
|
||||
|
|
@ -56,7 +56,7 @@ export const Tutorial: FC<TutorialProps> = (props) => {
|
|||
<Text type="secondary">Connect to your chat workspace</Text>
|
||||
</div>
|
||||
<Arrow />
|
||||
<div className={cx(styles.step)}>
|
||||
<div className={styles.step}>
|
||||
<PluginLink query={{ page: 'schedules' }}>
|
||||
<div className={cx(styles.icon, { [bem(styles.icon, 'active')]: step === TutorialStep.Schedules })}>
|
||||
<img src={scheduleIcon} />
|
||||
|
|
@ -65,7 +65,7 @@ export const Tutorial: FC<TutorialProps> = (props) => {
|
|||
<Text type="secondary">Add your team calendar to define an on-call rotation.</Text>
|
||||
</div>
|
||||
<Arrow />
|
||||
<div className={cx(styles.step)}>
|
||||
<div className={styles.step}>
|
||||
<PluginLink query={{ page: 'alert-groups' }}>
|
||||
<div className={cx('icon', { [bem(styles.icon, 'active')]: step === TutorialStep.Incidents })}>
|
||||
<img src={bellIcon} />
|
||||
|
|
@ -81,7 +81,7 @@ export const Tutorial: FC<TutorialProps> = (props) => {
|
|||
const Arrow = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<div className={cx(styles.arrow)}>
|
||||
<div className={styles.arrow}>
|
||||
<svg width="41" height="16" viewBox="0 0 41 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M40.7071 8.70711C41.0976 8.31658 41.0976 7.68342 40.7071 7.29289L34.3431 0.928932C33.9526 0.538408 33.3195 0.538408 32.9289 0.928932C32.5384 1.31946 32.5384 1.95262 32.9289 2.34315L38.5858 8L32.9289 13.6569C32.5384 14.0474 32.5384 14.6805 32.9289 15.0711C33.3195 15.4616 33.9526 15.4616 34.3431 15.0711L40.7071 8.70711ZM0 9H40V7H0V9Z" />
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2, OrgRole } from '@grafana/data';
|
||||
import { VerticalGroup, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'grafana/app/core/core';
|
||||
|
|
@ -16,9 +16,9 @@ export const Unauthorized: FC<Props> = ({ requiredUserAction: { permission, fall
|
|||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={cx(styles.notFound)}>
|
||||
<div className={styles.notFound}>
|
||||
<VerticalGroup spacing="lg" align="center">
|
||||
<Text.Title level={1} className={cx(styles.errorCode)}>
|
||||
<Text.Title level={1} className={styles.errorCode}>
|
||||
403
|
||||
</Text.Title>
|
||||
<Text.Title level={4}>
|
||||
|
|
|
|||
|
|
@ -95,14 +95,14 @@ export const UserGroups = (props: UserGroupsProps) => {
|
|||
};
|
||||
|
||||
const renderItem = (item: Item, index: number) => (
|
||||
<li className={cx(styles.user)}>
|
||||
<li className={styles.user}>
|
||||
{renderUser(item.data)}
|
||||
{!disabled && (
|
||||
<div className={cx(styles.userButtons)}>
|
||||
<div className={styles.userButtons}>
|
||||
<HorizontalGroup>
|
||||
<IconButton
|
||||
aria-label="Remove"
|
||||
className={cx(styles.icon)}
|
||||
className={styles.icon}
|
||||
name="trash-alt"
|
||||
onClick={getDeleteItemHandler(index)}
|
||||
/>
|
||||
|
|
@ -114,7 +114,7 @@ export const UserGroups = (props: UserGroupsProps) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={cx(styles.root)}>
|
||||
<div className={styles.root}>
|
||||
<VerticalGroup>
|
||||
{!disabled && (
|
||||
<RemoteSelect
|
||||
|
|
@ -133,7 +133,7 @@ export const UserGroups = (props: UserGroupsProps) => {
|
|||
renderItem={renderItem}
|
||||
axis="y"
|
||||
lockAxis="y"
|
||||
helperClass={cx(styles.sortable)}
|
||||
helperClass={styles.sortable}
|
||||
items={items}
|
||||
onSortEnd={onSortEnd}
|
||||
handleAddGroup={handleAddUserGroup}
|
||||
|
|
@ -178,7 +178,7 @@ export const SortableList = SortableContainer<SortableListProps>(
|
|||
}, [items]);
|
||||
|
||||
return (
|
||||
<ul className={cx(styles.groups)} ref={listRef}>
|
||||
<ul className={styles.groups} ref={listRef}>
|
||||
{items.map((item, index) =>
|
||||
item.type === 'item' ? (
|
||||
<SortableItem key={item.key} index={index}>
|
||||
|
|
@ -186,7 +186,7 @@ export const SortableList = SortableContainer<SortableListProps>(
|
|||
</SortableItem>
|
||||
) : isMultipleGroups ? (
|
||||
<SortableItem key={item.key} index={index}>
|
||||
<li className={cx(styles.separator)}>
|
||||
<li className={styles.separator}>
|
||||
<Text type="secondary">{item.data.name}</Text>
|
||||
</li>
|
||||
</SortableItem>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const VerticalTabsBar = (props: VerticalTabsBarProps) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={cx(styles.root)}>
|
||||
<div className={styles.root}>
|
||||
{React.Children.toArray(children)
|
||||
.filter(Boolean)
|
||||
.map((child: React.ReactElement, idx) => (
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
.body {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.responders-list {
|
||||
list-style-type: none;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
& > li .hover-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > li:hover .hover-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
& > li {
|
||||
padding: 10px 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > li:hover {
|
||||
background: var(--background-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-icon-background {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--timeline-icon-background);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
& > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&--green {
|
||||
background: #299c46;
|
||||
}
|
||||
}
|
||||
|
||||
.responder-name {
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.confirm-participant-invitation-modal {
|
||||
max-width: 550px;
|
||||
}
|
||||
|
||||
.confirm-participant-invitation-modal-select {
|
||||
display: inline-flex;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.learn-more-link {
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
export const getAddRespondersStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
content: css`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
`,
|
||||
|
||||
respondersList: css`
|
||||
padding-top: 8px;
|
||||
list-style-type: none;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
& > li .hover-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > li:hover .hover-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
& > li {
|
||||
padding: 10px 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > li:hover {
|
||||
background: ${theme.colors.background.secondary};
|
||||
}
|
||||
`,
|
||||
|
||||
alert: css`
|
||||
padding-top: 4px;
|
||||
`,
|
||||
|
||||
timelineIconBackground: css`
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(${theme.isDark ? '70, 76, 84, 1' : '70, 76, 84, 0'});
|
||||
|
||||
& > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&--green {
|
||||
background: #299c46;
|
||||
}
|
||||
`,
|
||||
|
||||
responderName: css`
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`,
|
||||
|
||||
confirmParticipantInvitationModal: css`
|
||||
max-width: 550px;
|
||||
`,
|
||||
|
||||
confirmParticipantInvitationModalSelect: css`
|
||||
display: inline-flex;
|
||||
margin: 0 4px;
|
||||
`,
|
||||
|
||||
learnMoreLink: css`
|
||||
display: inline-block;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { HorizontalGroup, Button, Modal, Alert, VerticalGroup, Icon } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { HorizontalGroup, Button, Modal, Alert, VerticalGroup, Icon, useStyles2 } from '@grafana/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -14,15 +13,13 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
|
|||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
|
||||
import styles from './AddResponders.module.scss';
|
||||
import { getAddRespondersStyles } from './AddResponders.styles';
|
||||
import { NotificationPolicyValue, UserResponder as UserResponderType } from './AddResponders.types';
|
||||
import { AddRespondersPopup } from './parts/AddRespondersPopup/AddRespondersPopup';
|
||||
import { NotificationPoliciesSelect } from './parts/NotificationPoliciesSelect/NotificationPoliciesSelect';
|
||||
import { TeamResponder } from './parts/TeamResponder/TeamResponder';
|
||||
import { UserResponder } from './parts/UserResponder/UserResponder';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
type Props = {
|
||||
mode: 'create' | 'update';
|
||||
hideAddResponderButton?: boolean;
|
||||
|
|
@ -31,21 +28,24 @@ type Props = {
|
|||
generateRemovePreviouslyPagedUserCallback?: (userId: string) => () => Promise<void>;
|
||||
};
|
||||
|
||||
const LearnMoreAboutNotificationPoliciesLink: React.FC = () => (
|
||||
<a
|
||||
className={cx('learn-more-link')}
|
||||
href="https://grafana.com/docs/oncall/latest/notify/#configure-user-notification-policies"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Text type="link">
|
||||
<HorizontalGroup spacing="xs">
|
||||
Learn more
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Text>
|
||||
</a>
|
||||
);
|
||||
const LearnMoreAboutNotificationPoliciesLink: React.FC = () => {
|
||||
const styles = useStyles2(getAddRespondersStyles);
|
||||
return (
|
||||
<a
|
||||
className={styles.learnMoreLink}
|
||||
href="https://grafana.com/docs/oncall/latest/notify/#configure-user-notification-policies"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Text type="link">
|
||||
<HorizontalGroup spacing="xs">
|
||||
Learn more
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Text>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddResponders = observer(
|
||||
({
|
||||
|
|
@ -56,6 +56,8 @@ export const AddResponders = observer(
|
|||
generateRemovePreviouslyPagedUserCallback,
|
||||
}: Props) => {
|
||||
const { directPagingStore } = useStore();
|
||||
const styles = useStyles2(getAddRespondersStyles);
|
||||
|
||||
const { selectedTeamResponder, selectedUserResponders } = directPagingStore;
|
||||
|
||||
const currentMoment = useMemo(() => dayjs(), []);
|
||||
|
|
@ -107,7 +109,7 @@ export const AddResponders = observer(
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('body')}>
|
||||
<div className={styles.content}>
|
||||
<Block bordered>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text.Title type="primary" level={4}>
|
||||
|
|
@ -129,7 +131,7 @@ export const AddResponders = observer(
|
|||
</HorizontalGroup>
|
||||
{(selectedTeamResponder || existingPagedUsers.length > 0 || selectedUserResponders.length > 0) && (
|
||||
<>
|
||||
<ul className={cx('responders-list')}>
|
||||
<ul className={styles.respondersList}>
|
||||
{selectedTeamResponder && (
|
||||
<TeamResponder team={selectedTeamResponder} handleDelete={directPagingStore.resetSelectedTeam} />
|
||||
)}
|
||||
|
|
@ -156,6 +158,7 @@ export const AddResponders = observer(
|
|||
{selectedUserResponders.length > 0 && (
|
||||
<Alert
|
||||
severity="info"
|
||||
className={styles.alert}
|
||||
title={
|
||||
(
|
||||
<Text type="primary">
|
||||
|
|
@ -184,7 +187,7 @@ export const AddResponders = observer(
|
|||
isOpen
|
||||
title="Confirm Participant Invitation"
|
||||
onDismiss={closeUserConfirmationModal}
|
||||
className={cx('confirm-participant-invitation-modal')}
|
||||
className={styles.confirmParticipantInvitationModal}
|
||||
>
|
||||
<VerticalGroup spacing="md">
|
||||
{!isCreateMode && (
|
||||
|
|
@ -194,7 +197,7 @@ export const AddResponders = observer(
|
|||
{currentMoment.tz(UserHelper.getTimezone(currentlyConsideredUser)).format('HH:mm')}) will be
|
||||
notified using
|
||||
</Text>
|
||||
<div className={cx('confirm-participant-invitation-modal-select')}>
|
||||
<div className={styles.confirmParticipantInvitationModalSelect}>
|
||||
<NotificationPoliciesSelect
|
||||
important={Boolean(currentlyConsideredUserNotificationPolicy)}
|
||||
onChange={onChangeCurrentlyConsideredUserNotificationPolicy}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
exports[`AddResponders should properly display the add responders button when hideAddResponderButton is false 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="body"
|
||||
class="css-1si66qn"
|
||||
>
|
||||
<div
|
||||
class="css-1x53p5e css-1x53p5e--bordered"
|
||||
|
|
@ -56,7 +56,7 @@ exports[`AddResponders should properly display the add responders button when hi
|
|||
exports[`AddResponders should properly display the add responders button when hideAddResponderButton is true 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="body"
|
||||
class="css-1si66qn"
|
||||
>
|
||||
<div
|
||||
class="css-1x53p5e css-1x53p5e--bordered"
|
||||
|
|
@ -90,7 +90,7 @@ exports[`AddResponders should properly display the add responders button when hi
|
|||
exports[`AddResponders should render properly in create mode 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="body"
|
||||
class="css-1si66qn"
|
||||
>
|
||||
<div
|
||||
class="css-1x53p5e css-1x53p5e--bordered"
|
||||
|
|
@ -143,7 +143,7 @@ exports[`AddResponders should render properly in create mode 1`] = `
|
|||
exports[`AddResponders should render properly in update mode 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="body"
|
||||
class="css-1si66qn"
|
||||
>
|
||||
<div
|
||||
class="css-1x53p5e css-1x53p5e--bordered"
|
||||
|
|
@ -196,7 +196,7 @@ exports[`AddResponders should render properly in update mode 1`] = `
|
|||
exports[`AddResponders should render selected team and users properly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="body"
|
||||
class="css-1si66qn"
|
||||
>
|
||||
<div
|
||||
class="css-1x53p5e css-1x53p5e--bordered"
|
||||
|
|
@ -239,7 +239,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
<ul
|
||||
class="responders-list"
|
||||
class="css-xp2upo"
|
||||
>
|
||||
<li>
|
||||
<div
|
||||
|
|
@ -257,7 +257,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
|
|||
class="css-18qv8yz-layoutChildrenWrapper"
|
||||
>
|
||||
<div
|
||||
class="timeline-icon-background"
|
||||
class="css-1yiiywv"
|
||||
>
|
||||
<img
|
||||
class="css-m6de9j css-m6de9j--medium"
|
||||
|
|
@ -270,7 +270,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
|
|||
class="css-18qv8yz-layoutChildrenWrapper"
|
||||
>
|
||||
<span
|
||||
class="css-77ouhj--undefined css-77ouhj--medium responder-name css-1287p17"
|
||||
class="css-77ouhj--undefined css-77ouhj--medium css-r15ga2"
|
||||
>
|
||||
my test team
|
||||
</span>
|
||||
|
|
@ -310,7 +310,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
|
|||
class="css-18qv8yz-layoutChildrenWrapper"
|
||||
>
|
||||
<div
|
||||
class="timeline-icon-background timeline-icon-background--green"
|
||||
class="css-1yiiywv timeline-icon-background--green"
|
||||
>
|
||||
<img
|
||||
class="css-m6de9j css-m6de9j--medium"
|
||||
|
|
@ -323,7 +323,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
|
|||
class="css-18qv8yz-layoutChildrenWrapper"
|
||||
>
|
||||
<span
|
||||
class="css-77ouhj--undefined css-77ouhj--medium responder-name css-1287p17"
|
||||
class="css-77ouhj--undefined css-77ouhj--medium css-r15ga2"
|
||||
>
|
||||
my test user3
|
||||
</span>
|
||||
|
|
@ -425,7 +425,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
|
|||
class="css-18qv8yz-layoutChildrenWrapper"
|
||||
>
|
||||
<div
|
||||
class="timeline-icon-background timeline-icon-background--green"
|
||||
class="css-1yiiywv timeline-icon-background--green"
|
||||
>
|
||||
<img
|
||||
class="css-m6de9j css-m6de9j--medium"
|
||||
|
|
@ -438,7 +438,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
|
|||
class="css-18qv8yz-layoutChildrenWrapper"
|
||||
>
|
||||
<span
|
||||
class="css-77ouhj--undefined css-77ouhj--medium responder-name css-1287p17"
|
||||
class="css-77ouhj--undefined css-77ouhj--medium css-r15ga2"
|
||||
>
|
||||
my test user
|
||||
</span>
|
||||
|
|
@ -539,7 +539,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
|
|||
class="css-18qv8yz-layoutChildrenWrapper"
|
||||
>
|
||||
<div
|
||||
class="timeline-icon-background timeline-icon-background--green"
|
||||
class="css-1yiiywv timeline-icon-background--green"
|
||||
>
|
||||
<img
|
||||
class="css-m6de9j css-m6de9j--medium"
|
||||
|
|
@ -552,7 +552,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
|
|||
class="css-18qv8yz-layoutChildrenWrapper"
|
||||
>
|
||||
<span
|
||||
class="css-77ouhj--undefined css-77ouhj--medium responder-name css-1287p17"
|
||||
class="css-77ouhj--undefined css-77ouhj--medium css-r15ga2"
|
||||
>
|
||||
my test user2
|
||||
</span>
|
||||
|
|
@ -639,7 +639,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
|
|||
</li>
|
||||
<div
|
||||
aria-label="[object Object]"
|
||||
class="css-10yjoiw"
|
||||
class="css-10yjoiw css-182y09v"
|
||||
data-testid="data-testid Alert info"
|
||||
role="status"
|
||||
>
|
||||
|
|
@ -667,7 +667,7 @@ exports[`AddResponders should render selected team and users properly 1`] = `
|
|||
class="css-77ouhj--primary css-77ouhj--medium css-1287p17"
|
||||
>
|
||||
<a
|
||||
class="learn-more-link"
|
||||
class="css-1cvxpvr"
|
||||
href="https://grafana.com/docs/oncall/latest/notify/#configure-user-notification-policies"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -1,35 +1,36 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { HorizontalGroup, IconButton } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { HorizontalGroup, IconButton, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { Avatar } from 'components/Avatar/Avatar';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import styles from 'containers/AddResponders/AddResponders.module.scss';
|
||||
import { getAddRespondersStyles } from 'containers/AddResponders/AddResponders.styles';
|
||||
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
type Props = {
|
||||
team: GrafanaTeam | null;
|
||||
handleDelete: React.MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
export const TeamResponder: FC<Props> = ({ team: { avatar_url, name }, handleDelete }) => (
|
||||
<li>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<div className={cx('timeline-icon-background')}>
|
||||
<Avatar size="medium" src={avatar_url} />
|
||||
</div>
|
||||
<Text className={cx('responder-name')}>{name}</Text>
|
||||
export const TeamResponder: FC<Props> = ({ team: { avatar_url, name }, handleDelete }) => {
|
||||
const styles = useStyles2(getAddRespondersStyles);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<div className={styles.timelineIconBackground}>
|
||||
<Avatar size="medium" src={avatar_url} />
|
||||
</div>
|
||||
<Text className={styles.responderName}>{name}</Text>
|
||||
</HorizontalGroup>
|
||||
<IconButton
|
||||
data-testid="team-responder-delete-icon"
|
||||
tooltip="Remove responder"
|
||||
name="trash-alt"
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<IconButton
|
||||
data-testid="team-responder-delete-icon"
|
||||
tooltip="Remove responder"
|
||||
name="trash-alt"
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</li>
|
||||
);
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ exports[`TeamResponder it renders data properly 1`] = `
|
|||
class="css-18qv8yz-layoutChildrenWrapper"
|
||||
>
|
||||
<div
|
||||
class="timeline-icon-background"
|
||||
class="css-1yiiywv"
|
||||
>
|
||||
<img
|
||||
class="css-m6de9j css-m6de9j--medium"
|
||||
|
|
@ -31,7 +31,7 @@ exports[`TeamResponder it renders data properly 1`] = `
|
|||
class="css-18qv8yz-layoutChildrenWrapper"
|
||||
>
|
||||
<span
|
||||
class="css-77ouhj--undefined css-77ouhj--medium responder-name css-1287p17"
|
||||
class="css-77ouhj--undefined css-77ouhj--medium css-r15ga2"
|
||||
>
|
||||
my test team
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { cx } from '@emotion/css';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { ActionMeta, HorizontalGroup, IconButton } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { ActionMeta, HorizontalGroup, IconButton, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { Avatar } from 'components/Avatar/Avatar';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import styles from 'containers/AddResponders/AddResponders.module.scss';
|
||||
import { getAddRespondersStyles } from 'containers/AddResponders/AddResponders.styles';
|
||||
import { UserResponder as UserResponderType } from 'containers/AddResponders/AddResponders.types';
|
||||
import { NotificationPoliciesSelect } from 'containers/AddResponders/parts/NotificationPoliciesSelect/NotificationPoliciesSelect';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
type Props = UserResponderType & {
|
||||
onImportantChange: (value: SelectableValue<number>, actionMeta: ActionMeta) => void | {};
|
||||
handleDelete: React.MouseEventHandler<HTMLButtonElement>;
|
||||
|
|
@ -24,28 +22,32 @@ export const UserResponder: FC<Props> = ({
|
|||
onImportantChange,
|
||||
handleDelete,
|
||||
disableNotificationPolicySelect = false,
|
||||
}) => (
|
||||
<li>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<div className={cx('timeline-icon-background', { 'timeline-icon-background--green': true })}>
|
||||
<Avatar size="medium" src={avatar} />
|
||||
</div>
|
||||
<Text className={cx('responder-name')}>{username}</Text>
|
||||
}) => {
|
||||
const styles = useStyles2(getAddRespondersStyles);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<div className={cx(styles.timelineIconBackground, { 'timeline-icon-background--green': true })}>
|
||||
<Avatar size="medium" src={avatar} />
|
||||
</div>
|
||||
<Text className={styles.responderName}>{username}</Text>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<NotificationPoliciesSelect
|
||||
disabled={disableNotificationPolicySelect}
|
||||
important={important}
|
||||
onChange={onImportantChange}
|
||||
/>
|
||||
<IconButton
|
||||
data-testid="user-responder-delete-icon"
|
||||
tooltip="Remove responder"
|
||||
name="trash-alt"
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<NotificationPoliciesSelect
|
||||
disabled={disableNotificationPolicySelect}
|
||||
important={important}
|
||||
onChange={onImportantChange}
|
||||
/>
|
||||
<IconButton
|
||||
data-testid="user-responder-delete-icon"
|
||||
tooltip="Remove responder"
|
||||
name="trash-alt"
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</li>
|
||||
);
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ exports[`UserResponder it renders data properly 1`] = `
|
|||
class="css-18qv8yz-layoutChildrenWrapper"
|
||||
>
|
||||
<div
|
||||
class="timeline-icon-background timeline-icon-background--green"
|
||||
class="css-1yiiywv timeline-icon-background--green"
|
||||
>
|
||||
<img
|
||||
class="css-m6de9j css-m6de9j--medium"
|
||||
|
|
@ -31,7 +31,7 @@ exports[`UserResponder it renders data properly 1`] = `
|
|||
class="css-18qv8yz-layoutChildrenWrapper"
|
||||
>
|
||||
<span
|
||||
class="css-77ouhj--undefined css-77ouhj--medium responder-name css-1287p17"
|
||||
class="css-77ouhj--undefined css-77ouhj--medium css-r15ga2"
|
||||
>
|
||||
johnsmith
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,94 +1,3 @@
|
|||
.root {
|
||||
background: var(--rotations-background);
|
||||
border: var(--rotations-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.current-time {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
background: var(--gradient-brandVertical);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
transition: left 500ms ease;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.layer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rotations {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.layer-title {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
padding: 8px;
|
||||
background: var(--secondary-background);
|
||||
}
|
||||
|
||||
.layer-title:hover {
|
||||
background: rgba(204, 204, 220, 0.12);
|
||||
}
|
||||
|
||||
.rotations-plus-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-plus-content {
|
||||
position: relative;
|
||||
padding-top: 26px;
|
||||
padding-bottom: 26px;
|
||||
}
|
||||
|
||||
.layer-header {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.layer-header-title {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: rgba(204, 204, 220, 0.65);
|
||||
}
|
||||
|
||||
.layer-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.add-rotations-layer {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-rotations-layer:hover {
|
||||
background: var(--secondary-background);
|
||||
}
|
||||
|
||||
/*
|
||||
animation
|
||||
*/
|
||||
|
||||
.enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
|||
93
grafana-plugin/src/containers/Rotations/Rotations.styles.ts
Normal file
93
grafana-plugin/src/containers/Rotations/Rotations.styles.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
export const getRotationsStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
root: css`
|
||||
background: 1px solid ${theme.colors.background.secondary};
|
||||
border: ${theme.colors.border.weak};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: ${theme.shape.radius.default};
|
||||
`,
|
||||
|
||||
currentTime: css`
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
background: ${theme.colors.gradients.brandVertical}
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
transition: left 500ms ease;
|
||||
`,
|
||||
|
||||
header: css`
|
||||
padding: 0 10px;
|
||||
`,
|
||||
|
||||
title: css`
|
||||
margin: 16px 0;
|
||||
`,
|
||||
|
||||
layer: css`
|
||||
display: block;
|
||||
`,
|
||||
|
||||
rotations: css`
|
||||
position: relative;
|
||||
`,
|
||||
|
||||
layerTitle: css`
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
padding: 8px;
|
||||
background: ${theme.colors.background.secondary};
|
||||
|
||||
&:hover {
|
||||
background: rgba(204, 204, 220, 0.12);
|
||||
}
|
||||
`,
|
||||
|
||||
rotationsPlusTitle: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`,
|
||||
|
||||
headerPlusContent: css`
|
||||
position: relative;
|
||||
padding-top: 26px;
|
||||
padding-bottom: 26px;
|
||||
`,
|
||||
|
||||
layerHeader: css`
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`,
|
||||
|
||||
layerHeaderTitle: css`
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: rgba(204, 204, 220, 0.65);
|
||||
`,
|
||||
|
||||
layerContent: css`
|
||||
position: relative;
|
||||
`,
|
||||
|
||||
addRotationsLayer: css`
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: ${theme.colors.background.secondary};
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { ValuePicker, HorizontalGroup, Button, Tooltip } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { ValuePicker, HorizontalGroup, Button, Tooltip, withTheme2 } from '@grafana/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
|
|
@ -22,10 +21,9 @@ import { UserActions } from 'utils/authorization/authorization';
|
|||
|
||||
import { DEFAULT_TRANSITION_TIMEOUT } from './Rotations.config';
|
||||
import { findColor } from './Rotations.helpers';
|
||||
import { getRotationsStyles } from './Rotations.styles';
|
||||
|
||||
import styles from './Rotations.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
import animationStyles from './Rotations.module.css';
|
||||
|
||||
interface RotationsProps extends WithStoreProps {
|
||||
shiftIdToShowRotationForm?: Shift['id'] | 'new';
|
||||
|
|
@ -41,6 +39,7 @@ interface RotationsProps extends WithStoreProps {
|
|||
disabled: boolean;
|
||||
filters: ScheduleFiltersType;
|
||||
onSlotClick?: (event: Event) => void;
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
interface RotationsState {
|
||||
|
|
@ -69,6 +68,7 @@ class _Rotations extends Component<RotationsProps, RotationsState> {
|
|||
filters,
|
||||
onShowShiftSwapForm,
|
||||
onSlotClick,
|
||||
theme,
|
||||
} = this.props;
|
||||
const { layerPriority, shiftStartToShowRotationForm, shiftEndToShowRotationForm } = this.state;
|
||||
|
||||
|
|
@ -97,13 +97,14 @@ class _Rotations extends Component<RotationsProps, RotationsState> {
|
|||
|
||||
const isTypeReadOnly =
|
||||
schedule && (schedule?.type === ScheduleType.Ical || schedule?.type === ScheduleType.Calendar);
|
||||
const styles = getRotationsStyles(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('header')}>
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<div className={cx('title')}>
|
||||
<div className={styles.title}>
|
||||
<Text.Title level={4} type="primary">
|
||||
Rotations
|
||||
</Text.Title>
|
||||
|
|
@ -145,28 +146,32 @@ class _Rotations extends Component<RotationsProps, RotationsState> {
|
|||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('rotations-plus-title')}>
|
||||
<div className={styles.rotationsPlusTitle}>
|
||||
{layers && layers.length ? (
|
||||
<TransitionGroup className={cx('layers')}>
|
||||
<TransitionGroup>
|
||||
{layers.map((layer, layerIndex) => (
|
||||
<CSSTransition key={layerIndex} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<div id={`layer${layer.priority}`} className={cx('layer')}>
|
||||
<div className={cx('layer-title')}>
|
||||
<CSSTransition
|
||||
key={layerIndex}
|
||||
timeout={DEFAULT_TRANSITION_TIMEOUT}
|
||||
classNames={{ ...animationStyles }}
|
||||
>
|
||||
<div id={`layer${layer.priority}`} className={styles.layer}>
|
||||
<div className={styles.layerTitle}>
|
||||
<HorizontalGroup spacing="sm" justify="center">
|
||||
<Text type="secondary">Layer {layer.priority}</Text>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('header-plus-content')}>
|
||||
<div className={styles.headerPlusContent}>
|
||||
<TimelineMarks />
|
||||
{!currentTimeHidden && (
|
||||
<div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />
|
||||
<div className={styles.currentTime} style={{ left: `${currentTimeX * 100}%` }} />
|
||||
)}
|
||||
<TransitionGroup className={cx('rotations')}>
|
||||
<TransitionGroup className={styles.rotations}>
|
||||
{layer.shifts.map(({ shiftId, isPreview, events }, rotationIndex) => (
|
||||
<CSSTransition
|
||||
key={rotationIndex}
|
||||
timeout={DEFAULT_TRANSITION_TIMEOUT}
|
||||
classNames={{ ...styles }}
|
||||
classNames={{ ...animationStyles }}
|
||||
>
|
||||
<Rotation
|
||||
onClick={(shiftStart, shiftEnd) => {
|
||||
|
|
@ -194,16 +199,16 @@ class _Rotations extends Component<RotationsProps, RotationsState> {
|
|||
</TransitionGroup>
|
||||
) : (
|
||||
<div>
|
||||
<div id={`layer1`} className={cx('layer')}>
|
||||
<div className={cx('layer-title')}>
|
||||
<div id={`layer1`} className={styles.layer}>
|
||||
<div className={styles.layerTitle}>
|
||||
<HorizontalGroup spacing="sm" justify="center">
|
||||
<Text type="secondary">Layer 1</Text>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('header-plus-content')}>
|
||||
<div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />
|
||||
<div className={styles.headerPlusContent}>
|
||||
<div className={styles.currentTime} style={{ left: `${currentTimeX * 100}%` }} />
|
||||
<TimelineMarks />
|
||||
<div className={cx('rotations')}>
|
||||
<div className={styles.rotations}>
|
||||
<Rotation
|
||||
onClick={(shiftStart, shiftEnd) => {
|
||||
this.handleAddLayer(nextPriority, shiftStart, shiftEnd);
|
||||
|
|
@ -219,7 +224,7 @@ class _Rotations extends Component<RotationsProps, RotationsState> {
|
|||
)}
|
||||
{nextPriority > 1 && (
|
||||
<div
|
||||
className={cx('add-rotations-layer')}
|
||||
className={styles.addRotationsLayer}
|
||||
onClick={() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
|
|
@ -232,6 +237,7 @@ class _Rotations extends Component<RotationsProps, RotationsState> {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{shiftIdToShowRotationForm && (
|
||||
<RotationForm
|
||||
shiftId={shiftIdToShowRotationForm}
|
||||
|
|
@ -338,4 +344,4 @@ class _Rotations extends Component<RotationsProps, RotationsState> {
|
|||
};
|
||||
}
|
||||
|
||||
export const Rotations = withMobXProviderContext(_Rotations);
|
||||
export const Rotations = withMobXProviderContext(withTheme2(_Rotations));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { HorizontalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { HorizontalGroup, useStyles2 } from '@grafana/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
|
|
@ -22,10 +21,9 @@ import { withMobXProviderContext } from 'state/withStore';
|
|||
|
||||
import { DEFAULT_TRANSITION_TIMEOUT } from './Rotations.config';
|
||||
import { findColor } from './Rotations.helpers';
|
||||
import { getRotationsStyles } from './Rotations.styles';
|
||||
|
||||
import styles from './Rotations.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
import animationStyles from './Rotations.module.css';
|
||||
|
||||
interface ScheduleFinalProps extends WithStoreProps {
|
||||
scheduleId: Schedule['id'];
|
||||
|
|
@ -45,6 +43,8 @@ const _ScheduleFinal: FC<ScheduleFinalProps> = observer(
|
|||
const base = 7 * 24 * 60; // in minutes
|
||||
const diff = currentDateInSelectedTimezone.diff(calendarStartDate, 'minutes');
|
||||
|
||||
const styles = useStyles2(getRotationsStyles);
|
||||
|
||||
const currentTimeX = diff / base;
|
||||
|
||||
const shifts = flattenShiftEvents(getShiftsFromStore(store, scheduleId, calendarStartDate));
|
||||
|
|
@ -62,11 +62,11 @@ const _ScheduleFinal: FC<ScheduleFinalProps> = observer(
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<div className={styles.root}>
|
||||
{!simplified && (
|
||||
<div className={cx('header')}>
|
||||
<div className={styles.header}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<div className={cx('title')}>
|
||||
<div className={styles.title}>
|
||||
<Text.Title level={4} type="primary">
|
||||
Final schedule
|
||||
</Text.Title>
|
||||
|
|
@ -74,14 +74,14 @@ const _ScheduleFinal: FC<ScheduleFinalProps> = observer(
|
|||
</HorizontalGroup>
|
||||
</div>
|
||||
)}
|
||||
<div className={cx('header-plus-content')}>
|
||||
{!currentTimeHidden && <div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<div className={styles.headerPlusContent}>
|
||||
{!currentTimeHidden && <div className={styles.currentTime} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<TimelineMarks />
|
||||
<TransitionGroup className={cx('rotations')}>
|
||||
<TransitionGroup className={styles.rotations}>
|
||||
{shifts && shifts.length ? (
|
||||
shifts.map(({ events }, index) => {
|
||||
return (
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...animationStyles }}>
|
||||
<Rotation
|
||||
key={index}
|
||||
events={events}
|
||||
|
|
@ -97,7 +97,7 @@ const _ScheduleFinal: FC<ScheduleFinalProps> = observer(
|
|||
);
|
||||
})
|
||||
) : (
|
||||
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...animationStyles }}>
|
||||
<Rotation events={[]} />
|
||||
</CSSTransition>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Tooltip } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, HorizontalGroup, Tooltip, withTheme2 } from '@grafana/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
|
|
@ -27,10 +27,9 @@ import { UserActions } from 'utils/authorization/authorization';
|
|||
|
||||
import { DEFAULT_TRANSITION_TIMEOUT } from './Rotations.config';
|
||||
import { findClosestUserEvent, findColor } from './Rotations.helpers';
|
||||
import { getRotationsStyles } from './Rotations.styles';
|
||||
|
||||
import styles from './Rotations.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
import animationStyles from './Rotations.module.css';
|
||||
|
||||
interface ScheduleOverridesProps extends WithStoreProps {
|
||||
shiftStartToShowOverrideForm: dayjs.Dayjs;
|
||||
|
|
@ -45,6 +44,7 @@ interface ScheduleOverridesProps extends WithStoreProps {
|
|||
disabled: boolean;
|
||||
disableShiftSwaps: boolean;
|
||||
filters: ScheduleFiltersType;
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
interface ScheduleOverridesState {
|
||||
|
|
@ -76,7 +76,9 @@ class _ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverr
|
|||
store: {
|
||||
userStore: { currentUserPk },
|
||||
},
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const { shiftStartToShowOverrideForm, shiftEndToShowOverrideForm } = this.state;
|
||||
|
||||
const shifts = getOverridesFromStore(store, scheduleId, store.timezoneStore.calendarStartDate) as ShiftEvents[];
|
||||
|
|
@ -98,13 +100,14 @@ class _ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverr
|
|||
const schedule = store.scheduleStore.items[scheduleId];
|
||||
|
||||
const isTypeReadOnly = !schedule?.enable_web_overrides;
|
||||
const styles = getRotationsStyles(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="overrides-list" className={cx('root')}>
|
||||
<div className={cx('header')}>
|
||||
<div id="overrides-list" className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<div className={cx('title')}>
|
||||
<div className={styles.title}>
|
||||
<Text.Title level={4} type="primary">
|
||||
Overrides and swaps
|
||||
</Text.Title>
|
||||
|
|
@ -147,13 +150,13 @@ class _ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverr
|
|||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('header-plus-content')}>
|
||||
{!currentTimeHidden && <div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<div className={styles.headerPlusContent}>
|
||||
{!currentTimeHidden && <div className={styles.currentTime} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<TimelineMarks />
|
||||
<TransitionGroup className={cx('rotations')}>
|
||||
<TransitionGroup className={styles.rotations}>
|
||||
{shiftSwaps && shiftSwaps.length
|
||||
? shiftSwaps.map(({ isPreview, events }, index) => (
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...animationStyles }}>
|
||||
<Rotation
|
||||
events={events}
|
||||
color={SHIFT_SWAP_COLOR}
|
||||
|
|
@ -170,10 +173,10 @@ class _ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverr
|
|||
))
|
||||
: null}
|
||||
</TransitionGroup>
|
||||
<TransitionGroup className={cx('rotations')}>
|
||||
<TransitionGroup className={styles.rotations}>
|
||||
{shifts && shifts.length ? (
|
||||
shifts.map(({ shiftId, isPreview, events }, index) => (
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...animationStyles }}>
|
||||
<Rotation
|
||||
events={events}
|
||||
color={getOverrideColor(index)}
|
||||
|
|
@ -186,7 +189,7 @@ class _ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverr
|
|||
</CSSTransition>
|
||||
))
|
||||
) : (
|
||||
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...animationStyles }}>
|
||||
<Rotation
|
||||
key={0}
|
||||
events={[]}
|
||||
|
|
@ -273,4 +276,4 @@ class _ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverr
|
|||
};
|
||||
}
|
||||
|
||||
export const ScheduleOverrides = withMobXProviderContext(_ScheduleOverrides);
|
||||
export const ScheduleOverrides = withMobXProviderContext(withTheme2(_ScheduleOverrides));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
|
||||
import { Badge, Button, HorizontalGroup, Icon } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { Badge, Button, HorizontalGroup, Icon, useStyles2 } from '@grafana/ui';
|
||||
import { observer } from 'mobx-react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||
|
|
@ -20,10 +19,9 @@ import { PLUGIN_ROOT } from 'utils/consts';
|
|||
import { useIsLoading } from 'utils/hooks';
|
||||
|
||||
import { DEFAULT_TRANSITION_TIMEOUT } from './Rotations.config';
|
||||
import { getRotationsStyles } from './Rotations.styles';
|
||||
|
||||
import styles from './Rotations.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
import animationStyles from './Rotations.module.css';
|
||||
|
||||
interface SchedulePersonalProps extends RouteComponentProps {
|
||||
userPk: ApiSchemas['User']['pk'];
|
||||
|
|
@ -78,10 +76,12 @@ const _SchedulePersonal: FC<SchedulePersonalProps> = observer(({ userPk, onSlotC
|
|||
|
||||
const emptyRotationsText = updatePersonalEventsLoading ? 'Loading ...' : 'There are no schedules relevant to user';
|
||||
|
||||
const styles = useStyles2(getRotationsStyles);
|
||||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('header')}>
|
||||
<div className={cx('title')}>
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">
|
||||
|
|
@ -116,14 +116,14 @@ const _SchedulePersonal: FC<SchedulePersonalProps> = observer(({ userPk, onSlotC
|
|||
</HorizontalGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('header-plus-content')}>
|
||||
{!currentTimeHidden && <div className={cx('current-time')} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<div className={styles.headerPlusContent}>
|
||||
{!currentTimeHidden && <div className={styles.currentTime} style={{ left: `${currentTimeX * 100}%` }} />}
|
||||
<TimelineMarks />
|
||||
<TransitionGroup className={cx('rotations')}>
|
||||
<TransitionGroup className={styles.rotations}>
|
||||
{shifts?.length ? (
|
||||
shifts.map(({ events }, index) => {
|
||||
return (
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<CSSTransition key={index} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...animationStyles }}>
|
||||
<Rotation
|
||||
simplified
|
||||
key={index}
|
||||
|
|
@ -137,7 +137,7 @@ const _SchedulePersonal: FC<SchedulePersonalProps> = observer(({ userPk, onSlotC
|
|||
);
|
||||
})
|
||||
) : (
|
||||
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...styles }}>
|
||||
<CSSTransition key={0} timeout={DEFAULT_TRANSITION_TIMEOUT} classNames={{ ...animationStyles }}>
|
||||
<Rotation events={[]} emptyText={emptyRotationsText} />
|
||||
</CSSTransition>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,128 +0,0 @@
|
|||
.root {
|
||||
height: 28px;
|
||||
background: #595959;
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
margin: 0 1px;
|
||||
padding: 4px;
|
||||
align-items: center;
|
||||
transition: opacity 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.working-hours {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.root__type_gap {
|
||||
background: rgba(209, 14, 92, 0.2);
|
||||
border: 1px dashed #ff5286;
|
||||
color: rgba(209, 14, 92, 0.5);
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.root__type_shift-swap {
|
||||
border-radius: 10px;
|
||||
background: #ff99002e;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.no-user {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--tag-background-primary);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.root__inactive {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.title {
|
||||
z-index: 1;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.label {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
padding: 2px 4px;
|
||||
line-height: 16px;
|
||||
z-index: 1;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.details {
|
||||
width: 300px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.details-user-status {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.details-user-status__type_success {
|
||||
background-color: var(--success-text-color);
|
||||
}
|
||||
|
||||
.time {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: white;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.is-oncall-icon {
|
||||
color: var(--oncall-icon-stroke-color);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.details-icon {
|
||||
width: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.username {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.second-column {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import React, { FC, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Icon, Tooltip, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, HorizontalGroup, Icon, Tooltip, useStyles2, VerticalGroup } from '@grafana/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
import { COLORS, getLabelCss } from 'styles/utils.styles';
|
||||
|
||||
import { Avatar } from 'components/Avatar/Avatar';
|
||||
import NonExistentUserName from 'components/NonExistentUserName/NonExistentUserName';
|
||||
|
|
@ -19,8 +21,6 @@ import { useStore } from 'state/useStore';
|
|||
|
||||
import { getTitle } from './ScheduleSlot.helpers';
|
||||
|
||||
import styles from './ScheduleSlot.module.css';
|
||||
|
||||
interface ScheduleSlotProps {
|
||||
event: Event;
|
||||
handleAddOverride: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
|
|
@ -33,13 +33,14 @@ interface ScheduleSlotProps {
|
|||
showScheduleNameAsSlotTitle?: boolean;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
const ONE_WEEK_IN_SECONDS = 7 * 24 * 60 * 60;
|
||||
|
||||
export const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
||||
const {
|
||||
timezoneStore: { getDateInSelectedTimezone },
|
||||
} = useStore();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const {
|
||||
event,
|
||||
color,
|
||||
|
|
@ -69,7 +70,7 @@ export const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
if (event.is_gap) {
|
||||
return (
|
||||
<Tooltip content={<ScheduleGapDetails event={event} />}>
|
||||
<div className={cx('root', 'root__type_gap')} />
|
||||
<div className={cx(styles.root, styles.gap)} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
@ -80,7 +81,7 @@ export const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
shouldRender={event.missing_users.length > 0}
|
||||
backupChildren={
|
||||
<div
|
||||
className={cx('root')}
|
||||
className={styles.root}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
|
|
@ -90,12 +91,12 @@ export const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
{event.missing_users.map((name) => (
|
||||
<div
|
||||
key={name}
|
||||
className={cx('root')}
|
||||
className={styles.root}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
>
|
||||
<div className={cx('title')}>
|
||||
<div className={styles.title}>
|
||||
<NonExistentUserName userName={name} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -122,7 +123,7 @@ export const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={cx('stack')} style={{ width: `${width * 100}%` }} onClick={onClick}>
|
||||
<div className={styles.stack} style={{ width: `${width * 100}%` }} onClick={onClick}>
|
||||
{renderEvent(event)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -137,6 +138,7 @@ const ShiftSwapEvent = (props: ShiftSwapEventProps) => {
|
|||
const { event, currentMoment } = props;
|
||||
|
||||
const store = useStore();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const shiftSwap = store.scheduleStore.shiftSwaps[event.shiftSwapId];
|
||||
|
||||
|
|
@ -159,14 +161,14 @@ const ShiftSwapEvent = (props: ShiftSwapEventProps) => {
|
|||
const benefactorStoreUser = store.userStore.items[shiftSwap?.benefactor?.pk];
|
||||
|
||||
const scheduleSlotContent = (
|
||||
<div className={cx('root', { 'root__type_shift-swap': true })} data-testid="schedule-slot">
|
||||
<div className={cx(styles.root, styles.swap)} data-testid="schedule-slot">
|
||||
{shiftSwap && (
|
||||
<HorizontalGroup spacing="xs">
|
||||
{beneficiary && <Avatar size="xs" src={beneficiary.avatar_full} />}
|
||||
{benefactor ? (
|
||||
<Avatar size="xs" src={benefactor.avatar_full} />
|
||||
) : (
|
||||
<div className={cx('no-user')}>
|
||||
<div className={styles.noUser}>
|
||||
<Text size="xs" type="primary">
|
||||
?
|
||||
</Text>
|
||||
|
|
@ -231,6 +233,7 @@ const RegularEvent = (props: RegularEventProps) => {
|
|||
showScheduleNameAsSlotTitle,
|
||||
} = props;
|
||||
const store = useStore();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { users } = event;
|
||||
|
||||
|
|
@ -268,7 +271,7 @@ const RegularEvent = (props: RegularEventProps) => {
|
|||
|
||||
const scheduleSlotContent = (
|
||||
<div
|
||||
className={cx('root', { root__inactive: inactive })}
|
||||
className={cx(styles.root, { [styles.inactive]: inactive })}
|
||||
style={{
|
||||
backgroundColor,
|
||||
}}
|
||||
|
|
@ -277,14 +280,14 @@ const RegularEvent = (props: RegularEventProps) => {
|
|||
>
|
||||
{storeUser && (!swap_request || swap_request.user) && (
|
||||
<WorkingHours
|
||||
className={cx('working-hours')}
|
||||
className={styles.workingHours}
|
||||
timezone={storeUser.timezone}
|
||||
workingHours={storeUser.working_hours}
|
||||
startMoment={start}
|
||||
duration={duration}
|
||||
/>
|
||||
)}
|
||||
<div className={cx('title')}>
|
||||
<div className={styles.title}>
|
||||
{swap_request && !swap_request.user ? <Icon name="user-arrows" /> : userTitle}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -374,6 +377,8 @@ const ScheduleSlotDetails = observer((props: ScheduleSlotDetailsProps) => {
|
|||
|
||||
const enableWebOverrides = schedule?.enable_web_overrides;
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
if (shiftId && !scheduleStore.shifts[shiftId]) {
|
||||
scheduleStore.updateOncallShift(shiftId);
|
||||
|
|
@ -390,47 +395,47 @@ const ScheduleSlotDetails = observer((props: ScheduleSlotDetailsProps) => {
|
|||
// const isOncall = Boolean(storeUser && onCallNow && onCallNow.some((onCallUser) => storeUser.pk === onCallUser.pk));
|
||||
|
||||
return (
|
||||
<div className={cx('details')}>
|
||||
<div className={styles.details}>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup>
|
||||
<div className={cx('details-icon')}>
|
||||
<div className={cx('badge')} style={{ backgroundColor: color }} />
|
||||
<div className={styles.detailsIcon}>
|
||||
<div className={styles.badge} style={{ backgroundColor: color }} />
|
||||
</div>
|
||||
<Text type="primary" maxWidth="222px">
|
||||
{title}
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<div className={cx('details-icon')}>
|
||||
<Icon className={cx('icon')} name={isShiftSwap ? 'user-arrows' : 'user'} />
|
||||
<div className={styles.detailsIcon}>
|
||||
<Icon className={styles.icon} name={isShiftSwap ? 'user-arrows' : 'user'} />
|
||||
</div>
|
||||
{isShiftSwap ? (
|
||||
<VerticalGroup spacing="xs">
|
||||
<Text type="primary">Swap pair</Text>
|
||||
<Text type="primary" className={cx('username')}>
|
||||
<Text type="primary" className={styles.username}>
|
||||
{beneficiaryName} <Text type="secondary"> (requested by)</Text>
|
||||
</Text>
|
||||
{benefactorName ? (
|
||||
<Text type="primary" className={cx('username')}>
|
||||
<Text type="primary" className={styles.username}>
|
||||
{benefactorName} <Text type="secondary"> (accepted by)</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="secondary" className={cx('username')}>
|
||||
<Text type="secondary" className={styles.username}>
|
||||
Not accepted yet
|
||||
</Text>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
) : (
|
||||
<Text type="primary" className={cx('username')}>
|
||||
<Text type="primary" className={styles.username}>
|
||||
{user?.username}
|
||||
</Text>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<div className={cx('details-icon')}>
|
||||
<Icon className={cx('icon')} name="clock-nine" />
|
||||
<div className={styles.detailsIcon}>
|
||||
<Icon className={styles.icon} name="clock-nine" />
|
||||
</div>
|
||||
<Text type="primary" className={cx('second-column')} data-testid="schedule-slot-user-local-time">
|
||||
<Text type="primary" className={styles.secondColumn} data-testid="schedule-slot-user-local-time">
|
||||
User's local time
|
||||
<br />
|
||||
{currentMoment.tz(user?.timezone).format('DD MMM, HH:mm')}
|
||||
|
|
@ -444,10 +449,10 @@ const ScheduleSlotDetails = observer((props: ScheduleSlotDetailsProps) => {
|
|||
</Text>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<div className={cx('details-icon')}>
|
||||
<Icon className={cx('icon')} name="arrows-h" />
|
||||
<div className={styles.detailsIcon}>
|
||||
<Icon className={styles.icon} name="arrows-h" />
|
||||
</div>
|
||||
<Text type="primary" className={cx('second-column')}>
|
||||
<Text type="primary" className={styles.secondColumn}>
|
||||
This shift
|
||||
<br />
|
||||
{dayjs(event.start).tz(user?.timezone).format('DD MMM, HH:mm')}
|
||||
|
|
@ -488,13 +493,14 @@ interface ScheduleGapDetailsProps {
|
|||
}
|
||||
|
||||
const ScheduleGapDetails = observer((props: ScheduleGapDetailsProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const {
|
||||
timezoneStore: { selectedTimezoneLabel, getDateInSelectedTimezone },
|
||||
} = useStore();
|
||||
const { event } = props;
|
||||
|
||||
return (
|
||||
<div className={cx('details')}>
|
||||
<div className={styles.details}>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup spacing="sm">
|
||||
<VerticalGroup spacing="none">
|
||||
|
|
@ -507,3 +513,138 @@ const ScheduleGapDetails = observer((props: ScheduleGapDetailsProps) => {
|
|||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
root: css`
|
||||
height: 28px;
|
||||
background: ${COLORS.GRAY_8};
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
margin: 0 1px;
|
||||
padding: 4px;
|
||||
align-items: center;
|
||||
transition: opacity 0.2s ease;
|
||||
cursor: pointer;
|
||||
`,
|
||||
|
||||
workingHours: css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
`,
|
||||
|
||||
stack: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
flex-shrink: 0;
|
||||
`,
|
||||
|
||||
// TODO: What would be a matching value from theme for background?
|
||||
gap: css`
|
||||
background: rgba(209, 14, 92, 0.2);
|
||||
border: 1px dashed ${theme.colors.error.text};
|
||||
color: rgba(209, 14, 92, 0.5);
|
||||
visibility: hidden;
|
||||
`,
|
||||
|
||||
// TODO: Same here
|
||||
swap: css`
|
||||
border-radius: 10px;
|
||||
background: #ff99002e;
|
||||
height: 20px;
|
||||
`,
|
||||
|
||||
noUser: css`
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: ${getLabelCss('blue', theme)};
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`,
|
||||
|
||||
inactive: css`
|
||||
opacity: 0.3;
|
||||
`,
|
||||
|
||||
title: css`
|
||||
z-index: 1;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
`,
|
||||
|
||||
label: css`
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
padding: 2px 4px;
|
||||
line-height: 16px;
|
||||
z-index: 1;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
margin-right: 5px;
|
||||
flex-shrink: 0;
|
||||
`,
|
||||
|
||||
details: css`
|
||||
width: 300px;
|
||||
padding: 5px 0;
|
||||
`,
|
||||
|
||||
detailsUserStatus: css`
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
|
||||
&--success {
|
||||
background-color: ${theme.colors.success.text};
|
||||
}
|
||||
`,
|
||||
|
||||
time: css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background-color: white;
|
||||
z-index: 2;
|
||||
`,
|
||||
|
||||
isOnCallIcon: css`
|
||||
color: ${theme.isDark ? '#181b1f' : '#fff'};
|
||||
vertical-align: middle;
|
||||
`,
|
||||
|
||||
detailsIcon: css`
|
||||
width: 16px;
|
||||
margin-right: 4px;
|
||||
`,
|
||||
|
||||
badge: css`
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
`,
|
||||
|
||||
username: css`
|
||||
word-break: break-word;
|
||||
`,
|
||||
|
||||
secondColumn: css`
|
||||
width: 120px;
|
||||
`,
|
||||
|
||||
icon: css`
|
||||
color: ${theme.colors.secondary.text};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ export const Header = observer(() => {
|
|||
<>
|
||||
<div>
|
||||
<div className={cx('page-header__inner', { [styles.headerTopNavbar]: isTopNavbar() })}>
|
||||
<div className={cx(styles.navbarLeft)}>
|
||||
<div className={styles.navbarLeft}>
|
||||
<span className={cx('page-header__logo', styles.logoContainer)}>
|
||||
<img className={cx(styles.pageHeaderImage)} src={logo} alt="Grafana OnCall" />
|
||||
<img className={styles.pageHeaderImage} src={logo} alt="Grafana OnCall" />
|
||||
</span>
|
||||
<div className={cx('page-header__info-block')}>{renderHeading()}</div>
|
||||
</div>
|
||||
|
|
@ -37,18 +37,18 @@ export const Header = observer(() => {
|
|||
if (store.isOpenSource) {
|
||||
return (
|
||||
<div className={cx('heading')}>
|
||||
<h1 className={cx(styles.pageHeaderTitle)}>Grafana OnCall</h1>
|
||||
<div className={cx(styles.navbarHeadingContainer)}>
|
||||
<h1 className={styles.pageHeaderTitle}>Grafana OnCall</h1>
|
||||
<div className={styles.navbarHeadingContainer}>
|
||||
<div className={cx('page-header__sub-title')}>{APP_SUBTITLE}</div>
|
||||
|
||||
<Card heading={undefined} className={cx(styles.navbarHeading)}>
|
||||
<Card heading={undefined} className={styles.navbarHeading}>
|
||||
<a
|
||||
href="https://github.com/grafana/oncall"
|
||||
className={cx(styles.navbarLink)}
|
||||
className={styles.navbarLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img src={gitHubStarSVG} className={cx(styles.navbarStarIcon)} alt="" /> Star us on GitHub
|
||||
<img src={gitHubStarSVG} className={styles.navbarStarIcon} alt="" /> Star us on GitHub
|
||||
</a>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
@ -59,7 +59,7 @@ export const Header = observer(() => {
|
|||
return (
|
||||
<>
|
||||
<HorizontalGroup>
|
||||
<h1 className={cx(styles.pageHeaderTitle)}>Grafana OnCall</h1>
|
||||
<h1 className={styles.pageHeaderTitle}>Grafana OnCall</h1>
|
||||
</HorizontalGroup>
|
||||
<div className={cx('page-header__sub-title')}>{APP_SUBTITLE}</div>
|
||||
</>
|
||||
|
|
@ -70,7 +70,7 @@ export const Header = observer(() => {
|
|||
const Banners: React.FC = () => {
|
||||
const styles = useStyles2(getHeaderStyles);
|
||||
return (
|
||||
<div className={cx(styles.banners)}>
|
||||
<div className={styles.banners}>
|
||||
<Alerts />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
.filters {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
.escalations {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
border: var(--border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.new-escalation-chain {
|
||||
margin: 16px;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.left-column {
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
border-right: var(--border);
|
||||
}
|
||||
|
||||
.escalations-list {
|
||||
overflow: auto;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.escalation {
|
||||
margin: 16px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
export const getEscalationChainStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
filters: css`
|
||||
margin-bottom: 20px;
|
||||
`,
|
||||
|
||||
loading: css`
|
||||
margin: 10px 20px;
|
||||
`,
|
||||
|
||||
escalations: css`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
border: 1px solid ${theme.colors.border.weak};
|
||||
border-radius: 2px;
|
||||
`,
|
||||
|
||||
newEscalationChain: css`
|
||||
margin: 16px;
|
||||
width: calc(100% - 32px);
|
||||
`,
|
||||
|
||||
leftColumn: css`
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid ${theme.colors.border.weak};
|
||||
`,
|
||||
|
||||
escalationsList: css`
|
||||
overflow: auto;
|
||||
max-height: 70vh;
|
||||
`,
|
||||
|
||||
escalation: css`
|
||||
margin: 16px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
`,
|
||||
|
||||
header: css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`,
|
||||
|
||||
list: css`
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
`,
|
||||
|
||||
buttons: css`
|
||||
padding-bottom: 24px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Icon, IconButton, Tooltip, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, HorizontalGroup, Icon, IconButton, Tooltip, VerticalGroup, withTheme2 } from '@grafana/ui';
|
||||
import { observer } from 'mobx-react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { getUtilStyles } from 'styles/utils.styles';
|
||||
|
||||
import { Collapse } from 'components/Collapse/Collapse';
|
||||
import { Block } from 'components/GBlock/Block';
|
||||
|
|
@ -30,11 +31,11 @@ import { withMobXProviderContext } from 'state/withStore';
|
|||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { PAGE, PLUGIN_ROOT } from 'utils/consts';
|
||||
|
||||
import styles from './EscalationChains.module.css';
|
||||
import { getEscalationChainStyles } from './EscalationChains.styles';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface EscalationChainsPageProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {}
|
||||
interface EscalationChainsPageProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
interface EscalationChainsPageState extends PageBaseState {
|
||||
modeToShowEscalationChainForm?: EscalationChainFormMode;
|
||||
|
|
@ -129,6 +130,7 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
|
|||
match: {
|
||||
params: { id },
|
||||
},
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const { extraEscalationChains } = this.state;
|
||||
|
|
@ -143,6 +145,9 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
|
|||
data = [...extraEscalationChains, ...searchResult];
|
||||
}
|
||||
|
||||
const styles = getEscalationChainStyles(theme);
|
||||
const utilStyles = getUtilStyles(theme);
|
||||
|
||||
return (
|
||||
<PageErrorHandlingWrapper
|
||||
errorData={errorData}
|
||||
|
|
@ -152,23 +157,23 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
|
|||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div>
|
||||
{this.renderFilters()}
|
||||
{!data || data.length ? (
|
||||
<div className={cx('escalations')}>
|
||||
<div className={cx('left-column')}>
|
||||
<div className={styles.escalations}>
|
||||
<div className={styles.leftColumn}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.setState({ modeToShowEscalationChainForm: EscalationChainFormMode.Create });
|
||||
}}
|
||||
icon="plus"
|
||||
className={cx('new-escalation-chain')}
|
||||
className={styles.newEscalationChain}
|
||||
>
|
||||
New escalation chain
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
<div className={cx('escalations-list')} data-testid="escalation-chains-list">
|
||||
<div className={styles.escalationsList} data-testid="escalation-chains-list">
|
||||
{data ? (
|
||||
<GList
|
||||
autoScroll
|
||||
|
|
@ -181,14 +186,14 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
|
|||
</GList>
|
||||
) : (
|
||||
<VerticalGroup>
|
||||
<Text type="primary" className={cx('loadingPlaceholder')}>
|
||||
<Text type="primary" className={utilStyles.loadingPlaceholder}>
|
||||
Loading...
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={cx('escalation')}>{this.renderEscalation()}</div>
|
||||
<div className={styles.escalation}>{this.renderEscalation()}</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tutorial
|
||||
|
|
@ -234,9 +239,11 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
|
|||
}
|
||||
|
||||
renderFilters() {
|
||||
const { query, store } = this.props;
|
||||
const { query, store, theme } = this.props;
|
||||
const styles = getEscalationChainStyles(theme);
|
||||
|
||||
return (
|
||||
<div className={cx('filters')}>
|
||||
<div className={styles.filters}>
|
||||
<RemoteFilters
|
||||
query={query}
|
||||
page={PAGE.Escalations}
|
||||
|
|
@ -286,7 +293,7 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
|
|||
};
|
||||
|
||||
renderEscalation = () => {
|
||||
const { store } = this.props;
|
||||
const { store, theme } = this.props;
|
||||
const { selectedEscalationChain } = this.state;
|
||||
|
||||
const { escalationChainStore } = store;
|
||||
|
|
@ -297,14 +304,15 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
|
|||
|
||||
const escalationChain = escalationChainStore.items[selectedEscalationChain];
|
||||
const escalationChainDetails = escalationChainStore.details[selectedEscalationChain];
|
||||
const styles = getEscalationChainStyles(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Block withBackground className={cx('header')}>
|
||||
<Block withBackground className={styles.header}>
|
||||
<Text size="large" onTextChange={this.handleEscalationChainNameChange} data-testid="escalation-chain-name">
|
||||
{escalationChain.name}
|
||||
</Text>
|
||||
<div className={cx('buttons')}>
|
||||
<div className={styles.buttons}>
|
||||
<HorizontalGroup>
|
||||
<WithPermissionControlTooltip userAction={UserActions.EscalationChainsWrite}>
|
||||
<IconButton
|
||||
|
|
@ -359,14 +367,14 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
|
|||
isOpen
|
||||
>
|
||||
{escalationChainDetails.length ? (
|
||||
<ul className={cx('list')}>
|
||||
<ul className={styles.list}>
|
||||
{escalationChainDetails.map((alertReceiveChannel) => (
|
||||
<li key={alertReceiveChannel.id}>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<PluginLink query={{ page: 'integrations', id: alertReceiveChannel.id }}>
|
||||
{alertReceiveChannel.display_name}
|
||||
</PluginLink>
|
||||
<ul className={cx('list')}>
|
||||
<ul className={styles.list}>
|
||||
{alertReceiveChannel.channel_filters.map((channelFilter) => (
|
||||
<li key={channelFilter.id}>
|
||||
<Icon name="arrow-right" />
|
||||
|
|
@ -472,4 +480,4 @@ class _EscalationChainsPage extends React.Component<EscalationChainsPageProps, E
|
|||
};
|
||||
}
|
||||
|
||||
export const EscalationChainsPage = withRouter(withMobXProviderContext(_EscalationChainsPage));
|
||||
export const EscalationChainsPage = withRouter(withMobXProviderContext(withTheme2(_EscalationChainsPage)));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, IconButton, Tooltip, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { Button, HorizontalGroup, IconButton, Tooltip, VerticalGroup, useStyles2 } from '@grafana/ui';
|
||||
import { getUtilStyles } from 'styles/utils.styles';
|
||||
|
||||
import { Avatar } from 'components/Avatar/Avatar';
|
||||
import { PluginLink } from 'components/PluginLink/PluginLink';
|
||||
|
|
@ -13,15 +14,15 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
|
|||
import { SilenceButtonCascader } from 'pages/incidents/parts/SilenceButtonCascader';
|
||||
import { move } from 'state/helpers';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { TEXT_ELLIPSIS_CLASS } from 'utils/consts';
|
||||
|
||||
import styles from './Incident.module.scss';
|
||||
export const IncidentRelatedUsers = (props: { incident: ApiSchemas['AlertGroup']; isFull: boolean }) => {
|
||||
const { incident, isFull } = props;
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
export function renderRelatedUsers(incident: ApiSchemas['AlertGroup'], isFull = false) {
|
||||
const { related_users } = incident;
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
const utilStyles = useStyles2(getUtilStyles);
|
||||
|
||||
let users = [...related_users];
|
||||
|
||||
if (!users.length && isFull) {
|
||||
|
|
@ -39,10 +40,10 @@ export function renderRelatedUsers(incident: ApiSchemas['AlertGroup'], isFull =
|
|||
return (
|
||||
<PluginLink key={user.pk} query={{ page: 'users', id: user.pk }} wrap={false}>
|
||||
<TextEllipsisTooltip placement="top" content={user.username}>
|
||||
<Text type="secondary" className={cx(TEXT_ELLIPSIS_CLASS)}>
|
||||
<Text type="secondary" className={utilStyles.overflowChild}>
|
||||
<Avatar size="small" src={user.avatar} />{' '}
|
||||
<span className={cx('break-word', 'u-margin-right-xs')}>{user.username}</span>
|
||||
<span className={cx('user-badge')}>{badge}</span>
|
||||
<span className={cx(utilStyles.wordBreakAll, 'u-margin-right-xs')}>{user.username}</span>
|
||||
<span className={styles.userBadge}>{badge}</span>
|
||||
</Text>
|
||||
</TextEllipsisTooltip>
|
||||
</PluginLink>
|
||||
|
|
@ -67,12 +68,16 @@ export function renderRelatedUsers(incident: ApiSchemas['AlertGroup'], isFull =
|
|||
const otherUsers = isFull ? [] : users.slice(2);
|
||||
|
||||
if (isFull) {
|
||||
return visibleUsers.map((user, index) => (
|
||||
return (
|
||||
<>
|
||||
{index ? ', ' : ''}
|
||||
{renderUser(user)}
|
||||
{visibleUsers.map((user, index) => (
|
||||
<>
|
||||
{index ? ', ' : ''}
|
||||
{renderUser(user)}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -101,7 +106,7 @@ export function renderRelatedUsers(incident: ApiSchemas['AlertGroup'], isFull =
|
|||
)}
|
||||
</VerticalGroup>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function getActionButtons(
|
||||
incident: ApiSchemas['AlertGroup'],
|
||||
|
|
@ -183,3 +188,11 @@ export function getActionButtons(
|
|||
|
||||
return <HorizontalGroup justify="flex-end">{buttons}</HorizontalGroup>;
|
||||
}
|
||||
|
||||
const getStyles = () => {
|
||||
return {
|
||||
userBadge: css`
|
||||
vertical-align: middle;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,209 +0,0 @@
|
|||
.incident-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.incident-row-left {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.block {
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.payload-subtitle {
|
||||
margin-bottom: var(--title-marginBottom);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid rgba(204, 204, 220, 0.15);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.buttons-row {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 16px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message a {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.message ul {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.message p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message code {
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.image {
|
||||
margin-top: 16px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
margin-top: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 50%;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.column:not(:first-child) {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.incidents-content > div:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(204, 204, 220, 0.25);
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.incidents-content > div:not(:first-child) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
list-style-type: none;
|
||||
margin: 0 0 24px 12px;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.not-found {
|
||||
margin: 50px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alert-group-stub {
|
||||
margin: 24px auto;
|
||||
width: 520px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alert-group-stub-divider {
|
||||
width: 520px;
|
||||
}
|
||||
|
||||
.timeline-icon-background {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--timeline-icon-background);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.blue {
|
||||
background: var(--tag-border-primary);
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.timeline-filter {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
color: var(--secondary-text-color);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.integration-logo {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.label-button {
|
||||
padding: 0 8px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.label-button:disabled {
|
||||
border: var(--border-strong);
|
||||
}
|
||||
|
||||
.label-button-text {
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.source-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-tag-container {
|
||||
margin-right: 8px;
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
height: 24px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.paged-users {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.paged-users-list {
|
||||
list-style-type: none;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
& > li .trash-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > li:hover .trash-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& > li {
|
||||
padding: 8px 12px;
|
||||
width: 100%;
|
||||
|
||||
& .hover-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > li:hover {
|
||||
background: var(--background-secondary);
|
||||
|
||||
& .hover-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-badge {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import React, { useState, SyntheticEvent } from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { LabelTag } from '@grafana/labels';
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -15,14 +17,16 @@ import {
|
|||
Modal,
|
||||
Tooltip,
|
||||
Divider,
|
||||
withTheme2,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import moment from 'moment-timezone';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import Emoji from 'react-emoji-render';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
import { COLORS, getLabelBackgroundTextColorObject } from 'styles/utils.styles';
|
||||
import { OnCallPluginExtensionPoints } from 'types';
|
||||
|
||||
import errorSVG from 'assets/img/error.svg';
|
||||
|
|
@ -63,12 +67,12 @@ import { parseURL } from 'utils/url';
|
|||
import { openNotification } from 'utils/utils';
|
||||
|
||||
import { getActionButtons } from './Incident.helpers';
|
||||
import styles from './Incident.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
const INTEGRATION_NAME_LENGTH_LIMIT = 30;
|
||||
|
||||
interface IncidentPageProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {}
|
||||
interface IncidentPageProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
interface IncidentPageState extends PageBaseState {
|
||||
showIntegrationSettings?: boolean;
|
||||
|
|
@ -134,6 +138,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
const { errorData, showIntegrationSettings, showAttachIncidentForm, silenceModalData } = this.state;
|
||||
const { isNotFoundError, isWrongTeamError, isUnknownError } = errorData;
|
||||
const { alerts } = store.alertGroupStore;
|
||||
const styles = getStyles(this.props.theme);
|
||||
|
||||
const incident = alerts.get(id);
|
||||
|
||||
|
|
@ -158,7 +163,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
|
||||
if (!incident && !isNotFoundError && !isWrongTeamError) {
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<div>
|
||||
<LoadingPlaceholder text="Loading Alert Group..." />
|
||||
</div>
|
||||
);
|
||||
|
|
@ -167,9 +172,9 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
return (
|
||||
<PageErrorHandlingWrapper errorData={errorData} objectName="alert group" pageName="incidents">
|
||||
{() => (
|
||||
<div className={cx('root')}>
|
||||
<div>
|
||||
{errorData.isNotFoundError ? (
|
||||
<div className={cx('not-found')}>
|
||||
<div className={styles.notFound}>
|
||||
<VerticalGroup spacing="lg" align="center">
|
||||
<Text.Title level={1}>404</Text.Title>
|
||||
<Text.Title level={4}>Alert group not found</Text.Title>
|
||||
|
|
@ -183,8 +188,8 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
) : (
|
||||
<>
|
||||
{this.renderHeader()}
|
||||
<div className={cx('content')}>
|
||||
<div className={cx('column')}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.column}>
|
||||
<Incident incident={incident} datetimeReference={this.getIncidentDatetimeReference(incident)} />
|
||||
<GroupedIncidentsList
|
||||
id={incident.pk}
|
||||
|
|
@ -192,7 +197,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
/>
|
||||
<AttachedIncidentsList id={incident.pk} getUnattachClickHandler={this.getUnattachClickHandler} />
|
||||
</div>
|
||||
<div className={cx('column')}>
|
||||
<div className={styles.column}>
|
||||
<VerticalGroup style={{ display: 'block' }}>
|
||||
{(!incident.resolved || incident?.paged_users?.length > 0) && (
|
||||
<AddResponders
|
||||
|
|
@ -291,21 +296,24 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
},
|
||||
} = this.props;
|
||||
const { alerts } = store.alertGroupStore;
|
||||
const styles = getStyles(this.props.theme);
|
||||
|
||||
const incident = alerts.get(id);
|
||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
||||
store.alertReceiveChannelStore,
|
||||
incident.alert_receive_channel
|
||||
);
|
||||
|
||||
const showLinkTo = !incident.dependent_alert_groups.length && !incident.root_alert_group && !incident.resolved;
|
||||
const integrationNameWithEmojies = <Emoji text={incident.alert_receive_channel.verbal_name} />;
|
||||
const sourceLink = incident?.render_for_web?.source_link;
|
||||
const isServiceNow = Boolean(incident?.external_urls?.find((el) => el.integration_type === INTEGRATION_SERVICENOW));
|
||||
|
||||
return (
|
||||
<Block className={cx('block')}>
|
||||
<Block className={styles.block}>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup className={cx('title')}>
|
||||
<HorizontalGroup>
|
||||
<PluginLink query={{ page: 'alert-groups', ...query }}>
|
||||
<IconButton aria-label="Go Back" name="arrow-left" size="xl" />
|
||||
</PluginLink>
|
||||
|
|
@ -337,11 +345,11 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
name="code-branch"
|
||||
onClick={this.showAttachIncidentForm}
|
||||
tooltip="Attach to another Alert Group"
|
||||
className={cx('title-icon')}
|
||||
className={styles.titleIcon}
|
||||
/>
|
||||
)}
|
||||
<a href={incident.slack_permalink} target="_blank" rel="noreferrer">
|
||||
<IconButton name="slack" tooltip="View in Slack" className={cx('title-icon')} />
|
||||
<IconButton name="slack" tooltip="View in Slack" className={styles.titleIcon} />
|
||||
</a>
|
||||
<CopyToClipboard
|
||||
text={window.location.href}
|
||||
|
|
@ -349,14 +357,14 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
openNotification('Link copied');
|
||||
}}
|
||||
>
|
||||
<IconButton name="copy" tooltip="Copy link" className={cx('title-icon')} />
|
||||
<IconButton name="copy" tooltip="Copy link" className={styles.titleIcon} />
|
||||
</CopyToClipboard>
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<div className={cx('info-row')}>
|
||||
<div className={styles.infoRow}>
|
||||
<HorizontalGroup>
|
||||
<div className={cx('status-tag-container')}>
|
||||
<div className={styles.statusTagContainer}>
|
||||
<IncidentDropdown
|
||||
alert={incident}
|
||||
onResolve={this.getOnActionButtonClick(incident.pk, AlertAction.Resolve)}
|
||||
|
|
@ -395,7 +403,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
variant="secondary"
|
||||
fill="outline"
|
||||
size="sm"
|
||||
className={cx('label-button')}
|
||||
className={styles.labelButton}
|
||||
>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
|
|
@ -405,18 +413,18 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
: 'Go to Integration'
|
||||
}
|
||||
>
|
||||
<div className={cx('label-button-text', 'source-name')}>
|
||||
<div className={cx('integration-logo')}>
|
||||
<div className={cx(styles.labelButtonText, styles.sourceName)}>
|
||||
<div className={styles.integrationLogo}>
|
||||
<IntegrationLogo integration={integration} scale={0.08} />
|
||||
</div>
|
||||
<div className={cx('label-button-text')}>{integrationNameWithEmojies}</div>
|
||||
<div className={styles.labelButtonText}>{integrationNameWithEmojies}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</PluginLink>
|
||||
|
||||
{isServiceNow && (
|
||||
<Button variant="secondary" fill="outline" size="sm" className={cx('label-button')}>
|
||||
<Button variant="secondary" fill="outline" size="sm" className={styles.labelButton}>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Icon name="exchange-alt" />
|
||||
<span>Service Now</span>
|
||||
|
|
@ -440,7 +448,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
fill="outline"
|
||||
size="sm"
|
||||
disabled={sourceLink === null || parseURL(sourceLink) === ''}
|
||||
className={cx('label-button')}
|
||||
className={styles.labelButton}
|
||||
icon="external-link-alt"
|
||||
>
|
||||
Source
|
||||
|
|
@ -451,7 +459,7 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
)}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<HorizontalGroup justify="space-between" className={cx('buttons-row')}>
|
||||
<HorizontalGroup justify="space-between" className={styles.buttonsRow}>
|
||||
<HorizontalGroup>
|
||||
{getActionButtons(incident, {
|
||||
onResolve: this.getOnActionButtonClick(incident.pk, AlertAction.Resolve),
|
||||
|
|
@ -516,8 +524,10 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
match: {
|
||||
params: { id },
|
||||
},
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const styles = getStyles(theme);
|
||||
const incident = store.alertGroupStore.alerts.get(id);
|
||||
|
||||
if (!incident.render_after_resolve_report_json) {
|
||||
|
|
@ -529,11 +539,11 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
const isResolutionNoteTextEmpty = resolutionNoteText === '';
|
||||
return (
|
||||
<Block bordered>
|
||||
<Text.Title type="primary" level={4} className={cx('timeline-title')}>
|
||||
<Text.Title type="primary" level={4} className={styles.timelineTitle}>
|
||||
Timeline
|
||||
</Text.Title>
|
||||
<RadioButtonGroup
|
||||
className={cx('timeline-filter')}
|
||||
className={styles.timelineFilter}
|
||||
options={[
|
||||
{ label: 'Show full timeline', value: 'all' },
|
||||
{ label: 'Resolution notes only', value: TimeLineRealm.ResolutionNote },
|
||||
|
|
@ -543,11 +553,15 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
this.setState({ timelineFilter: value });
|
||||
}}
|
||||
/>
|
||||
<ul className={cx('timeline')} data-testid="incident-timeline-list">
|
||||
<ul className={styles.timeline} data-testid="incident-timeline-list">
|
||||
{timeline.map((item: TimeLineItem, idx: number) => (
|
||||
<li key={idx} className={cx('timeline-item')}>
|
||||
<li key={idx} className={styles.timelineItem}>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<div className={cx('timeline-icon-background', { blue: item.realm === TimeLineRealm.ResolutionNote })}>
|
||||
<div
|
||||
className={cx(styles.timelineIconBackground, {
|
||||
blue: item.realm === TimeLineRealm.ResolutionNote,
|
||||
})}
|
||||
>
|
||||
{this.renderTimelineItemIcon(item.realm)}
|
||||
</div>
|
||||
<VerticalGroup spacing="none">
|
||||
|
|
@ -687,16 +701,17 @@ class _IncidentPage extends React.Component<IncidentPageProps, IncidentPageState
|
|||
}
|
||||
|
||||
function Incident({ incident }: { incident: ApiSchemas['AlertGroup']; datetimeReference: string }) {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<div key={incident.pk} className={cx('incident')}>
|
||||
<div key={incident.pk}>
|
||||
<div
|
||||
className={cx('message')}
|
||||
className={styles.message}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(incident.render_for_web.message),
|
||||
}}
|
||||
data-testid="incident-message"
|
||||
/>
|
||||
{incident.render_for_web.image_url && <img className={cx('image')} src={incident.render_for_web.image_url} />}
|
||||
{incident.render_for_web.image_url && <img className={styles.image} src={incident.render_for_web.image_url} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -710,6 +725,7 @@ function GroupedIncidentsList({
|
|||
}) {
|
||||
const store = useStore();
|
||||
const incident = store.alertGroupStore.alerts.get(id);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const alerts = incident.alerts;
|
||||
if (!alerts) {
|
||||
|
|
@ -722,7 +738,7 @@ function GroupedIncidentsList({
|
|||
return (
|
||||
<Collapse
|
||||
headerWithBackground
|
||||
className={cx('collapse')}
|
||||
className={styles.collapse}
|
||||
isOpen={false}
|
||||
label={
|
||||
<HorizontalGroup wrap>
|
||||
|
|
@ -731,7 +747,7 @@ function GroupedIncidentsList({
|
|||
<Text type="secondary">{latestAlertMoment.format('MMM DD, YYYY HH:mm:ss Z').toString()}</Text>
|
||||
</HorizontalGroup>
|
||||
}
|
||||
contentClassName={cx('incidents-content')}
|
||||
contentClassName={styles.incidentsContent}
|
||||
>
|
||||
{alerts.map((alert) => (
|
||||
<GroupedIncident key={alert.id} incident={alert} datetimeReference={getIncidentDatetimeReference(alert)} />
|
||||
|
|
@ -744,12 +760,13 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle
|
|||
const [incidentRawResponse, setIncidentRawResponse] = useState<{ id: string; raw_request_data: any }>(undefined);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const payloadJSON = isModalOpen ? JSON.stringify(incidentRawResponse.raw_request_data, null, 4) : undefined;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isModalOpen && (
|
||||
<Modal onDismiss={() => setIsModalOpen(false)} closeOnEscape isOpen={isModalOpen} title="Alert Payload">
|
||||
<div className={cx('payload-subtitle')}>
|
||||
<div className={styles.payloadSubtitle}>
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">
|
||||
{incident.render_for_web.title} - {datetimeReference}
|
||||
|
|
@ -765,7 +782,7 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle
|
|||
openNotification('Copied!');
|
||||
}}
|
||||
>
|
||||
<Button className={cx('button')} variant="primary" icon="copy">
|
||||
<Button variant="primary" icon="copy">
|
||||
Copy to Clipboard
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
|
|
@ -775,8 +792,8 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle
|
|||
)}
|
||||
|
||||
<div key={incident.id}>
|
||||
<div className={cx('incident-row')}>
|
||||
<div className={cx('incident-row-left')}>
|
||||
<div className={styles.incidentRow}>
|
||||
<div className={styles.incidentRowLeftSide}>
|
||||
<HorizontalGroup wrap justify={'flex-start'}>
|
||||
<Text.Title type="secondary" level={4}>
|
||||
{incident.render_for_web.title}
|
||||
|
|
@ -784,7 +801,7 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle
|
|||
<Text type="secondary">{datetimeReference}</Text>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('incident-row-right')}>
|
||||
<div>
|
||||
<HorizontalGroup wrap={false} justify={'flex-end'}>
|
||||
<Tooltip placement="top" content="Alert Payload">
|
||||
<IconButton aria-label="Alert Payload" name="arrow" onClick={() => openIncidentResponse(incident)} />
|
||||
|
|
@ -794,13 +811,13 @@ function GroupedIncident({ incident, datetimeReference }: { incident: GroupedAle
|
|||
</div>
|
||||
<Text type="secondary">
|
||||
<div
|
||||
className={cx('message')}
|
||||
className={styles.message}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(incident.render_for_web.message),
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
{incident.render_for_web.image_url && <img className={cx('image')} src={incident.render_for_web.image_url} />}
|
||||
{incident.render_for_web.image_url && <img className={styles.image} src={incident.render_for_web.image_url} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
@ -820,6 +837,7 @@ function AttachedIncidentsList({
|
|||
getUnattachClickHandler(pk: string): void;
|
||||
}) {
|
||||
const store = useStore();
|
||||
const styles = useStyles2(getStyles);
|
||||
const incident = store.alertGroupStore.alerts.get(id);
|
||||
|
||||
if (!incident.dependent_alert_groups.length) {
|
||||
|
|
@ -831,10 +849,10 @@ function AttachedIncidentsList({
|
|||
return (
|
||||
<Collapse
|
||||
headerWithBackground
|
||||
className={cx('collapse')}
|
||||
className={styles.collapse}
|
||||
isOpen
|
||||
label={<HorizontalGroup wrap>{incident.dependent_alert_groups.length} Attached Alert Groups</HorizontalGroup>}
|
||||
contentClassName={cx('incidents-content')}
|
||||
contentClassName={styles.incidentsContent}
|
||||
>
|
||||
{alerts.map((incident) => {
|
||||
return (
|
||||
|
|
@ -855,8 +873,9 @@ function AttachedIncidentsList({
|
|||
}
|
||||
|
||||
const AlertGroupStub = ({ buttons }: { buttons: React.ReactNode }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<div className={cx('alert-group-stub')}>
|
||||
<div className={styles.alertGroupStub}>
|
||||
<VerticalGroup align="center" spacing="md">
|
||||
<img src={errorSVG} alt="" />
|
||||
<Text.Title level={3}>An unexpected error happened</Text.Title>
|
||||
|
|
@ -864,7 +883,7 @@ const AlertGroupStub = ({ buttons }: { buttons: React.ReactNode }) => {
|
|||
OnCall is not able to receive any information about the current Alert Group. It's unknown if it's firing,
|
||||
acknowledged, silenced, or resolved.
|
||||
</Text>
|
||||
<div className={cx('alert-group-stub-divider')}>
|
||||
<div className={styles.alertGroupStubDivider}>
|
||||
<Divider />
|
||||
</div>
|
||||
<Text type="secondary">Meanwhile, you could try changing the status of this Alert Group:</Text>
|
||||
|
|
@ -876,4 +895,221 @@ const AlertGroupStub = ({ buttons }: { buttons: React.ReactNode }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const IncidentPage = withRouter(withMobXProviderContext(_IncidentPage));
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
incidentRow: css`
|
||||
display: flex;
|
||||
`,
|
||||
|
||||
incidentRowLeftSide: css`
|
||||
flex-grow: 1;
|
||||
`,
|
||||
|
||||
block: css`
|
||||
padding: 0 0 20px 0;
|
||||
`,
|
||||
|
||||
payloadSubtitle: css`
|
||||
margin-bottom: 16px;
|
||||
`,
|
||||
|
||||
infoRow: css`
|
||||
width: 100%;
|
||||
border-bottom: 1px solid ${theme.colors.border.medium};
|
||||
padding-bottom: 20px;
|
||||
`,
|
||||
|
||||
buttonsRow: css`
|
||||
margin-top: 20px;
|
||||
`,
|
||||
|
||||
content: css`
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
`,
|
||||
|
||||
timelineIconBackground: css`
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(${theme.isDark ? '70, 76, 84, 1' : '70, 76, 84, 0'});
|
||||
`,
|
||||
|
||||
message: css`
|
||||
margin-top: 16px;
|
||||
word-wrap: break-word;
|
||||
|
||||
a {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: break-spaces;
|
||||
}
|
||||
`,
|
||||
|
||||
image: css`
|
||||
margin-top: 16px;
|
||||
max-width: 100%;
|
||||
`,
|
||||
|
||||
collapse: css`
|
||||
margin-top: 16px;
|
||||
position: relative;
|
||||
`,
|
||||
|
||||
column: css`
|
||||
width: 50%;
|
||||
padding-right: 24px;
|
||||
|
||||
&:not(:first-child) {
|
||||
padding-left: 24px;
|
||||
}
|
||||
`,
|
||||
|
||||
incidentsContent: css`
|
||||
> div:not(:last-child) {
|
||||
border-bottom: 1px solid ${COLORS.BORDER};
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
> div:not(:first-child) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
`,
|
||||
|
||||
timeline: css`
|
||||
list-style-type: none;
|
||||
margin: 0 0 24px 12px;
|
||||
`,
|
||||
|
||||
timelineItem: css`
|
||||
margin-top: 12px;
|
||||
`,
|
||||
|
||||
notFound: css`
|
||||
margin: 50px auto;
|
||||
text-align: center;
|
||||
`,
|
||||
|
||||
alertGroupStub: css`
|
||||
margin: 24px auto;
|
||||
width: 520px;
|
||||
text-align: center;
|
||||
`,
|
||||
|
||||
alertGroupStubDivider: css`
|
||||
width: 520px;
|
||||
`,
|
||||
|
||||
blue: css`
|
||||
background: ${getLabelBackgroundTextColorObject('blue', theme).sourceColor};
|
||||
`,
|
||||
|
||||
timelineTitle: css`
|
||||
margin-bottom: 24px;
|
||||
`,
|
||||
|
||||
timelineFilter: css`
|
||||
margin-bottom: 24px;
|
||||
`,
|
||||
|
||||
titleIcon: css`
|
||||
color: ${theme.colors.secondary.text};
|
||||
margin-left: 4px;
|
||||
`,
|
||||
|
||||
integrationLogo: css`
|
||||
margin-right: 8px;
|
||||
`,
|
||||
|
||||
labelButton: css`
|
||||
padding: 0 8px;
|
||||
font-weight: 400;
|
||||
|
||||
&:disabled {
|
||||
border: 1px solid ${theme.colors.border.strong};
|
||||
}
|
||||
`,
|
||||
|
||||
labelButtonText: css`
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`,
|
||||
|
||||
sourceName: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`,
|
||||
|
||||
statusTagContainer: css`
|
||||
margin-right: 8px;
|
||||
display: inherit;
|
||||
`,
|
||||
|
||||
statusTag: css`
|
||||
height: 24px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 2px;
|
||||
`,
|
||||
|
||||
pagedUsers: css`
|
||||
width: 100%;
|
||||
`,
|
||||
|
||||
// TODO: Where are trash-button/hover-button coming from?
|
||||
pagedUsersList: css`
|
||||
list-style-type: none;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
& > li .trash-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > li:hover .trash-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& > li {
|
||||
padding: 8px 12px;
|
||||
width: 100%;
|
||||
|
||||
& .hover-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& > li:hover {
|
||||
background: ${theme.colors.background.secondary};
|
||||
|
||||
& .hover-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
userBadge: css`
|
||||
vertical-align: middle;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
export const IncidentPage = withRouter(withMobXProviderContext(withTheme2(_IncidentPage)));
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
.select {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.fields-dropdown {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.above-incidents-table {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.horizontal-scroll-table table td:global(.rc-table-cell) {
|
||||
white-space: nowrap;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.bulk-actions-container {
|
||||
margin: 10px 0 10px 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bulk-actions-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.other-users {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 24px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.btn-results {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* filter cards */
|
||||
|
||||
.cards {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.col {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
display: block;
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
.loadingPlaceholder {
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.col {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.col {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { SyntheticEvent } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { LabelTag } from '@grafana/labels';
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -8,17 +9,16 @@ import {
|
|||
Icon,
|
||||
LoadingPlaceholder,
|
||||
RadioButtonGroup,
|
||||
Themeable2,
|
||||
Tooltip,
|
||||
VerticalGroup,
|
||||
withTheme2,
|
||||
} from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { capitalize } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
import moment from 'moment-timezone';
|
||||
import Emoji from 'react-emoji-render';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { getUtilStyles } from 'styles/utils.styles';
|
||||
|
||||
import { CardButton } from 'components/CardButton/CardButton';
|
||||
import { CursorPagination } from 'components/CursorPagination/CursorPagination';
|
||||
|
|
@ -48,8 +48,9 @@ import {
|
|||
import { ActionKey } from 'models/loader/action-keys';
|
||||
import { LoaderHelper } from 'models/loader/loader.helpers';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { renderRelatedUsers } from 'pages/incident/Incident.helpers';
|
||||
import { IncidentRelatedUsers } from 'pages/incident/Incident.helpers';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { RootStore } from 'state/rootStore';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { LocationHelper } from 'utils/LocationHelper';
|
||||
|
|
@ -58,18 +59,17 @@ import { INCIDENT_HORIZONTAL_SCROLLING_STORAGE, PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS
|
|||
import { getItem, setItem } from 'utils/localStorage';
|
||||
import { TableColumn } from 'utils/types';
|
||||
|
||||
import styles from './Incidents.module.scss';
|
||||
import { IncidentDropdown } from './parts/IncidentDropdown';
|
||||
import { SilenceButtonCascader } from './parts/SilenceButtonCascader';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface Pagination {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
interface IncidentsPageProps extends WithStoreProps, PageProps, RouteComponentProps, Themeable2 {}
|
||||
interface IncidentsPageProps extends WithStoreProps, PageProps, RouteComponentProps {
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
interface IncidentsPageState {
|
||||
selectedIncidentIds: Array<ApiSchemas['AlertGroup']['pk']>;
|
||||
|
|
@ -164,14 +164,17 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
render() {
|
||||
const { history } = this.props;
|
||||
const { showAddAlertGroupForm } = this.state;
|
||||
|
||||
const {
|
||||
theme,
|
||||
store: { alertReceiveChannelStore },
|
||||
} = this.props;
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('title')}>
|
||||
<div>
|
||||
<div className={styles.title}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text.Title level={3}>Alert Groups</Text.Title>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsDirectPaging}>
|
||||
|
|
@ -200,15 +203,16 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
);
|
||||
}
|
||||
|
||||
renderCards(filtersState, setFiltersState, filtersOnFiltersValueChange, store) {
|
||||
renderCards(filtersState, setFiltersState, filtersOnFiltersValueChange, store: RootStore, theme: GrafanaTheme2) {
|
||||
const { values } = filtersState;
|
||||
const { stats } = store.alertGroupStore;
|
||||
|
||||
const status = values.status || [];
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<div className={cx('cards', 'row')}>
|
||||
<div key="new" className={cx('col')}>
|
||||
<div className={cx(styles.cards, styles.row)}>
|
||||
<div key="new" className={styles.col}>
|
||||
<CardButton
|
||||
icon={<Icon name="bell" size="xxl" />}
|
||||
description="Firing"
|
||||
|
|
@ -222,7 +226,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
<div key="acknowledged" className={cx('col')}>
|
||||
<div key="acknowledged" className={styles.col}>
|
||||
<CardButton
|
||||
icon={<Icon name="eye" size="xxl" />}
|
||||
description="Acknowledged"
|
||||
|
|
@ -236,7 +240,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
<div key="resolved" className={cx('col')}>
|
||||
<div key="resolved" className={styles.col}>
|
||||
<CardButton
|
||||
icon={<Icon name="check" size="xxl" />}
|
||||
description="Resolved"
|
||||
|
|
@ -250,7 +254,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
<div key="silenced" className={cx('col')}>
|
||||
<div key="silenced" className={styles.col}>
|
||||
<CardButton
|
||||
icon={<Icon name="bell-slash" size="xxl" />}
|
||||
description="Silenced"
|
||||
|
|
@ -307,15 +311,17 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
};
|
||||
|
||||
renderIncidentFilters() {
|
||||
const { query, store } = this.props;
|
||||
const { query, store, theme } = this.props;
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<div className={cx('filters')}>
|
||||
<div className={styles.filters}>
|
||||
<RemoteFilters
|
||||
query={query}
|
||||
page={PAGE.Incidents}
|
||||
onChange={this.handleFiltersChange}
|
||||
extraFilters={(...args) => {
|
||||
return this.renderCards(...args, store);
|
||||
return this.renderCards(...args, store, theme);
|
||||
}}
|
||||
grafanaTeamStore={store.grafanaTeamStore}
|
||||
defaultFilters={{
|
||||
|
|
@ -424,7 +430,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
|
||||
renderBulkActions = () => {
|
||||
const { selectedIncidentIds, affectedRows, isHorizontalScrolling } = this.state;
|
||||
const { store } = this.props;
|
||||
const { store, theme } = this.props;
|
||||
|
||||
if (!store.alertGroupStore.bulkActions) {
|
||||
return null;
|
||||
|
|
@ -439,10 +445,12 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
Object.keys(affectedRows).length
|
||||
);
|
||||
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<div className={cx('above-incidents-table')}>
|
||||
<div className={cx('bulk-actions-container')}>
|
||||
<div className={cx('bulk-actions-list')}>
|
||||
<div className={styles.aboveIncidentsTable}>
|
||||
<div className={styles.bulkActionsContainer}>
|
||||
<div className={styles.bulkActionsList}>
|
||||
{'resolve' in store.alertGroupStore.bulkActions && (
|
||||
<WithPermissionControlTooltip key="resolve" userAction={UserActions.AlertGroupsWrite}>
|
||||
<Button
|
||||
|
|
@ -491,18 +499,18 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('fields-dropdown')}>
|
||||
<div className={styles.fieldsDropdown}>
|
||||
<RenderConditionally shouldRender={!isLoading && hasInvalidatedAlert}>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Text type="secondary">Results out of date</Text>
|
||||
<Button className={cx('btn-results')} variant="primary" onClick={this.onIncidentsUpdateClick}>
|
||||
<Button className={styles.btnResults} variant="primary" onClick={this.onIncidentsUpdateClick}>
|
||||
Refresh
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</RenderConditionally>
|
||||
|
||||
<RenderConditionally shouldRender={isLoading}>
|
||||
<LoadingPlaceholder text="Loading..." className={cx('loadingPlaceholder')} />
|
||||
<LoadingPlaceholder text="Loading..." className={styles.loadingPlaceholder} />
|
||||
</RenderConditionally>
|
||||
|
||||
<RenderConditionally shouldRender={store.hasFeature(AppFeature.Labels)}>
|
||||
|
|
@ -522,11 +530,14 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
renderTable() {
|
||||
const { selectedIncidentIds, pagination, isHorizontalScrolling } = this.state;
|
||||
const { alertGroupStore, filtersStore, loaderStore } = this.props.store;
|
||||
const { theme } = this.props;
|
||||
|
||||
const { results, prev, next } = AlertGroupHelper.getAlertSearchResult(alertGroupStore);
|
||||
const isLoading =
|
||||
LoaderHelper.isLoading(loaderStore, ActionKey.FETCH_INCIDENTS) || filtersStore.options['incidents'] === undefined;
|
||||
|
||||
const styles = getStyles(theme);
|
||||
|
||||
if (results && !results.length) {
|
||||
return (
|
||||
<Tutorial
|
||||
|
|
@ -551,7 +562,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
const tableColumns = this.getTableColumns();
|
||||
|
||||
return (
|
||||
<div className={cx('root')} ref={this.rootElRef}>
|
||||
<div ref={this.rootElRef}>
|
||||
{this.renderBulkActions()}
|
||||
<GTable
|
||||
emptyText={isLoading ? 'Loading...' : 'No alert groups found'}
|
||||
|
|
@ -567,7 +578,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
scroll={{ x: isHorizontalScrolling ? 'max-content' : undefined }}
|
||||
/>
|
||||
{this.shouldShowPagination() && (
|
||||
<div className={cx('pagination')}>
|
||||
<div className={styles.pagination}>
|
||||
<CursorPagination
|
||||
current={`${pagination.start}-${pagination.end}`}
|
||||
itemsPerPage={alertGroupStore.alertsSearchResult?.page_size}
|
||||
|
|
@ -583,15 +594,16 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
);
|
||||
}
|
||||
|
||||
renderId(record: ApiSchemas['AlertGroup']) {
|
||||
renderId = (record: ApiSchemas['AlertGroup']) => {
|
||||
const styles = getUtilStyles(this.props.theme);
|
||||
return (
|
||||
<TextEllipsisTooltip placement="top" content={`#${record.inside_organization_number}`}>
|
||||
<Text type="secondary" className={cx(TEXT_ELLIPSIS_CLASS, 'overflow-child--line-1')}>
|
||||
<Text type="secondary" className={cx(styles.overflowChild)}>
|
||||
#{record.inside_organization_number}
|
||||
</Text>
|
||||
</TextEllipsisTooltip>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderTitle = (record: ApiSchemas['AlertGroup']) => {
|
||||
const { store, query } = this.props;
|
||||
|
|
@ -601,7 +613,7 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
return (
|
||||
<div>
|
||||
<TextEllipsisTooltip placement="top" content={record.render_for_web.title}>
|
||||
<Text type="link" size="medium" className={cx('overflow-parent')} data-testid="integration-url">
|
||||
<Text type="link" size="medium" data-testid="integration-url">
|
||||
<PluginLink
|
||||
query={{
|
||||
page: 'alert-groups',
|
||||
|
|
@ -627,16 +639,18 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
|
||||
renderSource = (record: ApiSchemas['AlertGroup']) => {
|
||||
const {
|
||||
theme,
|
||||
store: { alertReceiveChannelStore },
|
||||
} = this.props;
|
||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
||||
alertReceiveChannelStore,
|
||||
record.alert_receive_channel
|
||||
);
|
||||
const utilStyles = getUtilStyles(theme);
|
||||
|
||||
return (
|
||||
<TextEllipsisTooltip
|
||||
className={cx('u-flex', 'u-flex-gap-xs', 'overflow-parent')}
|
||||
className={cx(utilStyles.flex, utilStyles.flexGapXS)}
|
||||
placement="top"
|
||||
content={record?.alert_receive_channel?.verbal_name || ''}
|
||||
>
|
||||
|
|
@ -831,7 +845,9 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
Users: {
|
||||
title: 'Users',
|
||||
key: 'users',
|
||||
render: renderRelatedUsers,
|
||||
render: (item: ApiSchemas['AlertGroup'], isFull: boolean) => (
|
||||
<IncidentRelatedUsers incident={item} isFull={isFull} />
|
||||
),
|
||||
grow: 1.5,
|
||||
},
|
||||
};
|
||||
|
|
@ -997,4 +1013,119 @@ class _IncidentsPage extends React.Component<IncidentsPageProps, IncidentsPageSt
|
|||
}
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
select: css`
|
||||
width: 400px;
|
||||
`,
|
||||
|
||||
bau: css`
|
||||
${[1, 2, 3].map(
|
||||
(num) => `
|
||||
$--line-${num} {
|
||||
-webkit-line-clamp: ${num}
|
||||
}
|
||||
`
|
||||
)}
|
||||
`,
|
||||
|
||||
actionButtons: css`
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
`,
|
||||
|
||||
filters: css`
|
||||
margin-bottom: 20px;
|
||||
`,
|
||||
|
||||
fieldsDropdown: css`
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
padding-left: 4px;
|
||||
`,
|
||||
|
||||
aboveIncidentsTable: css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`,
|
||||
|
||||
horizontalScrollTable: css`
|
||||
table td:global(.rc-table-cell) {
|
||||
white-space: nowrap;
|
||||
padding-right: 16px;
|
||||
}
|
||||
`,
|
||||
|
||||
bulkActionsContainer: css`
|
||||
margin: 10px 0 10px 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
`,
|
||||
|
||||
bulkActionsList: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`,
|
||||
|
||||
otherUsers: css`
|
||||
color: ${theme.colors.secondary.text};
|
||||
`,
|
||||
|
||||
pagination: css`
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
`,
|
||||
|
||||
title: css`
|
||||
margin-bottom: 24px;
|
||||
right: 0;
|
||||
`,
|
||||
|
||||
btnResults: css`
|
||||
margin-left: 8px;
|
||||
`,
|
||||
|
||||
/* filter cards */
|
||||
|
||||
cards: css`
|
||||
margin-top: 25px;
|
||||
`,
|
||||
|
||||
row: css`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
row-gap: 16px;
|
||||
`,
|
||||
|
||||
loadingPlaceholder: css`
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
`,
|
||||
|
||||
col: css`
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
display: block;
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
export const IncidentsPage = withRouter(withMobXProviderContext(withTheme2(_IncidentsPage)));
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ function IncidentStatusTag({
|
|||
return (
|
||||
<Tag
|
||||
forwardedRef={forwardedRef}
|
||||
className={cx(styles.incidentTag)}
|
||||
className={styles.incidentTag}
|
||||
color={getIncidentTagColor(alert)}
|
||||
onClick={() => {
|
||||
const boundingRect = forwardedRef.current.getBoundingClientRect();
|
||||
|
|
@ -52,7 +52,7 @@ function IncidentStatusTag({
|
|||
}}
|
||||
>
|
||||
<Text size="small">{IncidentStatus[alert.status]}</Text>
|
||||
<Icon className={cx(styles.incidentIcon)} name="angle-down" size="sm" />
|
||||
<Icon className={styles.incidentIcon} name="angle-down" size="sm" />
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
|
@ -104,12 +104,12 @@ export const IncidentDropdown: FC<{
|
|||
<div className={cx(styles.incidentOptions, { [utilStyles.disabled]: isLoading })}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<div
|
||||
className={cx(styles.incidentOptionItem)}
|
||||
className={styles.incidentOptionItem}
|
||||
onClick={(e) => onClickFn(e, AlertAction.Resolve, onUnresolve, IncidentStatus.Firing)}
|
||||
>
|
||||
Firing{' '}
|
||||
{currentLoadingAction === IncidentStatus.Firing && isLoading && (
|
||||
<span className={cx(styles.incidentOptionEl)}>
|
||||
<span className={styles.incidentOptionEl}>
|
||||
<LoadingPlaceholder text="" />
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -131,12 +131,12 @@ export const IncidentDropdown: FC<{
|
|||
<div className={cx(styles.incidentOptions, { [utilStyles.disabled]: isLoading })}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<div
|
||||
className={cx(styles.incidentOptionItem)}
|
||||
className={styles.incidentOptionItem}
|
||||
onClick={(e) => onClickFn(e, AlertAction.Acknowledge, onUnacknowledge, IncidentStatus.Firing)}
|
||||
>
|
||||
Unacknowledge{' '}
|
||||
{currentLoadingAction === IncidentStatus.Firing && isLoading && (
|
||||
<span className={cx(styles.incidentOptionEl)}>
|
||||
<span className={styles.incidentOptionEl}>
|
||||
<LoadingPlaceholder text="" />
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -144,12 +144,12 @@ export const IncidentDropdown: FC<{
|
|||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<div
|
||||
className={cx(styles.incidentOptionItem)}
|
||||
className={styles.incidentOptionItem}
|
||||
onClick={(e) => onClickFn(e, AlertAction.Acknowledge, onResolve, IncidentStatus.Resolved)}
|
||||
>
|
||||
Resolve{' '}
|
||||
{currentLoadingAction === IncidentStatus.Resolved && isLoading && (
|
||||
<span className={cx(styles.incidentOptionEl)}>
|
||||
<span className={styles.incidentOptionEl}>
|
||||
<LoadingPlaceholder text="" />
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -172,12 +172,12 @@ export const IncidentDropdown: FC<{
|
|||
<div className={cx(styles.incidentOptions, { [utilStyles.disabled]: isLoading })}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<div
|
||||
className={cx(styles.incidentOptionItem)}
|
||||
className={styles.incidentOptionItem}
|
||||
onClick={(e) => onClickFn(e, AlertAction.unResolve, onAcknowledge, IncidentStatus.Acknowledged)}
|
||||
>
|
||||
Acknowledge{' '}
|
||||
{currentLoadingAction === IncidentStatus.Acknowledged && isLoading && (
|
||||
<span className={cx(styles.incidentOptionEl)}>
|
||||
<span className={styles.incidentOptionEl}>
|
||||
<LoadingPlaceholder text="" />
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -185,19 +185,19 @@ export const IncidentDropdown: FC<{
|
|||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<div
|
||||
className={cx(styles.incidentOptionItem)}
|
||||
className={styles.incidentOptionItem}
|
||||
onClick={(e) => onClickFn(e, AlertAction.unResolve, onResolve, IncidentStatus.Resolved)}
|
||||
>
|
||||
Resolve{' '}
|
||||
{currentLoadingAction === IncidentStatus.Resolved && isLoading && (
|
||||
<span className={cx(styles.incidentOptionEl)}>
|
||||
<span className={styles.incidentOptionEl}>
|
||||
<LoadingPlaceholder text="" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
|
||||
<div className={cx(styles.incidentOptionItem)}>
|
||||
<div className={styles.incidentOptionItem}>
|
||||
<SilenceSelect
|
||||
customValueNum={CUSTOM_SILENCE_VALUE}
|
||||
placeholder={
|
||||
|
|
@ -249,12 +249,12 @@ export const IncidentDropdown: FC<{
|
|||
<div className={cx(styles.incidentOptions, { [utilStyles.disabled]: isLoading })}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<div
|
||||
className={cx(styles.incidentOptionItem)}
|
||||
className={styles.incidentOptionItem}
|
||||
onClick={(e) => onClickFn(e, AlertAction.Silence, onUnsilence, IncidentStatus.Firing)}
|
||||
>
|
||||
Unsilence{' '}
|
||||
{currentLoadingAction === IncidentStatus.Firing && isLoading && (
|
||||
<span className={cx(styles.incidentOptionEl)}>
|
||||
<span className={styles.incidentOptionEl}>
|
||||
<LoadingPlaceholder text="" />
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -262,12 +262,12 @@ export const IncidentDropdown: FC<{
|
|||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<div
|
||||
className={cx(styles.incidentOptionItem)}
|
||||
className={styles.incidentOptionItem}
|
||||
onClick={(e) => onClickFn(e, AlertAction.Silence, onAcknowledge, IncidentStatus.Acknowledged)}
|
||||
>
|
||||
Acknowledge{' '}
|
||||
{currentLoadingAction === IncidentStatus.Acknowledged && isLoading && (
|
||||
<span className={cx(styles.incidentOptionEl)}>
|
||||
<span className={styles.incidentOptionEl}>
|
||||
<LoadingPlaceholder text="" />
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -275,12 +275,12 @@ export const IncidentDropdown: FC<{
|
|||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip userAction={UserActions.AlertGroupsWrite}>
|
||||
<div
|
||||
className={cx(styles.incidentOptionItem)}
|
||||
className={styles.incidentOptionItem}
|
||||
onClick={(e) => onClickFn(e, AlertAction.Silence, onAcknowledge, IncidentStatus.Resolved)}
|
||||
>
|
||||
Resolve{' '}
|
||||
{currentLoadingAction === IncidentStatus.Resolved && isLoading && (
|
||||
<span className={cx(styles.incidentOptionEl)}>
|
||||
<span className={styles.incidentOptionEl}>
|
||||
<LoadingPlaceholder text="" />
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
.newIntegrationButton {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.heartbeat-badge {
|
||||
padding: 4px 10px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tabsBar {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.integrations-header {
|
||||
margin-bottom: 24px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.integrations-table-row {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.integrations-table {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.integrations-actionsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 200px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.integrations-actionItem {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
border-left: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
min-width: 84px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: row;
|
||||
|
||||
&:hover {
|
||||
background: var(--cards-background);
|
||||
}
|
||||
}
|
||||
|
||||
.goToDirectPagingAlert {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { COLORS } from 'styles/utils.styles';
|
||||
|
||||
export const getIntegrationsStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
newIntegrationButton: css`
|
||||
width: 180px;
|
||||
`,
|
||||
|
||||
heartbeatBadge: css`
|
||||
padding: 4px 10px;
|
||||
width: 40px;
|
||||
`,
|
||||
|
||||
title: css`
|
||||
margin-bottom: 16px;
|
||||
`,
|
||||
|
||||
tabsBar: css`
|
||||
margin-bottom: 24px;
|
||||
`,
|
||||
|
||||
integrationsHeader: css`
|
||||
margin-bottom: 24px;
|
||||
right: 0;
|
||||
`,
|
||||
|
||||
integrationsTableRow: css`
|
||||
height: 40px;
|
||||
`,
|
||||
|
||||
integrationsTable: css`
|
||||
margin-top: 16px;
|
||||
`,
|
||||
|
||||
integrationsActionsList: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 200px;
|
||||
border-radius: 2px;
|
||||
`,
|
||||
|
||||
integrationsActionItem: css`
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
border-left: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
min-width: 84px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: row;
|
||||
|
||||
&:hover {
|
||||
background: ${theme.isLight ? 'rgba(244, 245, 245)' : COLORS.GRAY_9};
|
||||
}
|
||||
`,
|
||||
|
||||
goToDirectPagingAlert: css`
|
||||
margin-top: 24px;
|
||||
`,
|
||||
|
||||
buttons: css`
|
||||
padding-bottom: 24px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { cx } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import {
|
||||
HorizontalGroup,
|
||||
Button,
|
||||
|
|
@ -11,13 +13,14 @@ import {
|
|||
TabsBar,
|
||||
TabContent,
|
||||
Alert,
|
||||
withTheme2,
|
||||
} from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import Emoji from 'react-emoji-render';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { getUtilStyles } from 'styles/utils.styles';
|
||||
|
||||
import { GTable } from 'components/GTable/GTable';
|
||||
import { HamburgerMenuIcon } from 'components/HamburgerMenuIcon/HamburgerMenuIcon';
|
||||
|
|
@ -54,7 +57,7 @@ import { UserActions } from 'utils/authorization/authorization';
|
|||
import { PAGE, TEXT_ELLIPSIS_CLASS } from 'utils/consts';
|
||||
import { openNotification } from 'utils/utils';
|
||||
|
||||
import styles from './Integrations.module.scss';
|
||||
import { getIntegrationsStyles } from './Integrations.styles';
|
||||
|
||||
enum TabType {
|
||||
MonitoringSystems = 'monitoring-systems',
|
||||
|
|
@ -74,7 +77,6 @@ const TABS = [
|
|||
},
|
||||
];
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
const FILTERS_DEBOUNCE_MS = 500;
|
||||
|
||||
interface IntegrationsState extends PageBaseState {
|
||||
|
|
@ -94,7 +96,9 @@ interface IntegrationsState extends PageBaseState {
|
|||
activeTab: TabType;
|
||||
}
|
||||
|
||||
interface IntegrationsProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {}
|
||||
interface IntegrationsProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
@observer
|
||||
class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsState> {
|
||||
|
|
@ -198,7 +202,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
};
|
||||
|
||||
render() {
|
||||
const { store, query } = this.props;
|
||||
const { store, query, theme } = this.props;
|
||||
const {
|
||||
alertReceiveChannelId,
|
||||
alertReceiveChannelIdToShowLabels,
|
||||
|
|
@ -206,15 +210,18 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
activeTab,
|
||||
integrationsFilters,
|
||||
} = this.state;
|
||||
|
||||
const { alertReceiveChannelStore } = store;
|
||||
const { count, results, page_size } = AlertReceiveChannelHelper.getPaginatedSearchResult(alertReceiveChannelStore);
|
||||
const isDirectPagingSelectedOnMonitoringSystemsTab =
|
||||
activeTab === TabType.MonitoringSystems && integrationsFilters.integration?.includes('direct_paging');
|
||||
|
||||
const styles = getIntegrationsStyles(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('title')}>
|
||||
<div>
|
||||
<div className={styles.title}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<VerticalGroup>
|
||||
<Text.Title level={3}>Integrations</Text.Title>
|
||||
|
|
@ -228,7 +235,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
this.setState({ alertReceiveChannelId: 'new' });
|
||||
}}
|
||||
icon="plus"
|
||||
className={cx('newIntegrationButton')}
|
||||
className={styles.newIntegrationButton}
|
||||
>
|
||||
New integration
|
||||
</Button>
|
||||
|
|
@ -236,7 +243,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
</HorizontalGroup>
|
||||
</div>
|
||||
<div>
|
||||
<TabsBar className={cx('tabsBar')}>
|
||||
<TabsBar className={styles.tabsBar}>
|
||||
{TABS.map(({ label, value }) => (
|
||||
<Tab
|
||||
key={value}
|
||||
|
|
@ -259,7 +266,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
/>
|
||||
{isDirectPagingSelectedOnMonitoringSystemsTab && (
|
||||
<Alert
|
||||
className={cx('goToDirectPagingAlert')}
|
||||
className={styles.goToDirectPagingAlert}
|
||||
severity="info"
|
||||
title="Direct Paging integrations have been moved."
|
||||
>
|
||||
|
|
@ -278,8 +285,8 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
rowKey="id"
|
||||
data={results}
|
||||
columns={this.getTableColumns(store.hasFeature)}
|
||||
className={cx('integrations-table')}
|
||||
rowClassName={cx('integrations-table-row')}
|
||||
className={styles.integrationsTable}
|
||||
rowClassName={styles.integrationsTableRow}
|
||||
pagination={{
|
||||
page: store.filtersStore.currentTablePageNum[PAGE.Integrations],
|
||||
total: results ? Math.ceil((count || 0) / page_size) : 0,
|
||||
|
|
@ -386,15 +393,19 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
);
|
||||
}
|
||||
|
||||
renderIntegrationStatus(item: ApiSchemas['AlertReceiveChannel'], alertReceiveChannelStore: AlertReceiveChannelStore) {
|
||||
renderIntegrationStatus = (
|
||||
item: ApiSchemas['AlertReceiveChannel'],
|
||||
alertReceiveChannelStore: AlertReceiveChannelStore
|
||||
) => {
|
||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[item.id];
|
||||
|
||||
let routesCounter = item.routes_count;
|
||||
let connectedEscalationsChainsCount = item.connected_escalations_chains_count;
|
||||
|
||||
return (
|
||||
<HorizontalGroup spacing="xs">
|
||||
{alertReceiveChannelCounter && (
|
||||
<PluginLink query={{ page: 'incidents', integration: item.id }} className={cx('alertsInfoText')}>
|
||||
<PluginLink query={{ page: 'incidents', integration: item.id }}>
|
||||
<TooltipBadge
|
||||
borderType="primary"
|
||||
placement="top"
|
||||
|
|
@ -431,10 +442,10 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
)}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderHeartbeat(item: ApiSchemas['AlertReceiveChannel']) {
|
||||
const { store } = this.props;
|
||||
renderHeartbeat = (item: ApiSchemas['AlertReceiveChannel']) => {
|
||||
const { store, theme } = this.props;
|
||||
const { alertReceiveChannelStore, heartbeatStore } = store;
|
||||
const alertReceiveChannel = alertReceiveChannelStore.items[item.id];
|
||||
|
||||
|
|
@ -442,13 +453,15 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
const heartbeat = heartbeatStore.items[heartbeatId];
|
||||
|
||||
const heartbeatStatus = Boolean(heartbeat?.status);
|
||||
const styles = getIntegrationsStyles(theme);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{alertReceiveChannel.is_available_for_integration_heartbeat && heartbeat?.last_heartbeat_time_verbal && (
|
||||
<TooltipBadge
|
||||
testId="heartbeat-badge"
|
||||
text={undefined}
|
||||
className={cx('heartbeat-badge')}
|
||||
className={styles.heartbeatBadge}
|
||||
placement="top"
|
||||
borderType={heartbeatStatus ? 'success' : 'danger'}
|
||||
customIcon={heartbeatStatus ? <HeartIcon /> : <HeartRedIcon />}
|
||||
|
|
@ -458,14 +471,16 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderMaintenance(item: ApiSchemas['AlertReceiveChannel']) {
|
||||
renderMaintenance = (item: ApiSchemas['AlertReceiveChannel']) => {
|
||||
const { theme } = this.props;
|
||||
const maintenanceMode = item.maintenance_mode;
|
||||
const utilStyles = getUtilStyles(theme);
|
||||
|
||||
if (maintenanceMode === MaintenanceMode.Debug || maintenanceMode === MaintenanceMode.Maintenance) {
|
||||
return (
|
||||
<div className={cx('u-flex')}>
|
||||
<div className={utilStyles.flex}>
|
||||
<TooltipBadge
|
||||
borderType="primary"
|
||||
icon="pause"
|
||||
|
|
@ -479,39 +494,41 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
renderTeam(item: ApiSchemas['AlertReceiveChannel'], teams: any) {
|
||||
renderTeam = (item: ApiSchemas['AlertReceiveChannel'], teams: any) => {
|
||||
return (
|
||||
<TextEllipsisTooltip placement="top" content={teams[item.team]?.name}>
|
||||
<TeamName className={TEXT_ELLIPSIS_CLASS} team={teams[item.team]} />
|
||||
</TextEllipsisTooltip>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderButtons = (item: ApiSchemas['AlertReceiveChannel']) => {
|
||||
const { store } = this.props;
|
||||
const { store, theme } = this.props;
|
||||
const styles = getIntegrationsStyles(theme);
|
||||
const utilStyles = getUtilStyles(theme);
|
||||
|
||||
return (
|
||||
<WithContextMenu
|
||||
renderMenuItems={() => (
|
||||
<div className={cx('integrations-actionsList')}>
|
||||
<div className={styles.integrationsActionsList}>
|
||||
<WithPermissionControlTooltip key="edit" userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integrations-actionItem')} onClick={() => this.onIntegrationEditClick(item.id)}>
|
||||
<div className={styles.integrationsActionItem} onClick={() => this.onIntegrationEditClick(item.id)}>
|
||||
<Text type="primary">Integration settings</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
|
||||
{store.hasFeature(AppFeature.Labels) && (
|
||||
<WithPermissionControlTooltip key="edit" userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integrations-actionItem')} onClick={() => this.onLabelsEditClick(item.id)}>
|
||||
<div className={styles.integrationsActionItem} onClick={() => this.onLabelsEditClick(item.id)}>
|
||||
<Text type="primary">Alert group labeling</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
<CopyToClipboard text={item.id} onCopy={() => openNotification('Integration ID has been copied')}>
|
||||
<div className={cx('integrations-actionItem')}>
|
||||
<div className={styles.integrationsActionItem}>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Icon name="copy" />
|
||||
|
||||
|
|
@ -520,9 +537,9 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
</div>
|
||||
</CopyToClipboard>
|
||||
<RenderConditionally shouldRender={item.allow_delete}>
|
||||
<div className={cx('thin-line-break')} />
|
||||
<div className={utilStyles.thinLineBreak} />
|
||||
<WithPermissionControlTooltip key="delete" userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integrations-actionItem')}>
|
||||
<div className={styles.integrationsActionItem}>
|
||||
<div
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
|
|
@ -560,12 +577,16 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
);
|
||||
};
|
||||
|
||||
getTableColumns = (hasFeatureFn) => {
|
||||
getTableColumns = (hasFeatureFn: (feature: string) => boolean) => {
|
||||
const {
|
||||
grafanaTeamStore,
|
||||
alertReceiveChannelStore,
|
||||
filtersStore: { applyLabelFilter },
|
||||
} = this.props.store;
|
||||
|
||||
const { theme } = this.props;
|
||||
|
||||
const styles = getIntegrationsStyles(theme);
|
||||
const isMonitoringSystemsTab = this.state.activeTab === TabType.MonitoringSystems;
|
||||
|
||||
const columns = [
|
||||
|
|
@ -614,7 +635,7 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
width: '50px',
|
||||
key: 'buttons',
|
||||
render: (item: ApiSchemas['AlertReceiveChannel']) => this.renderButtons(item),
|
||||
className: cx('buttons'),
|
||||
className: styles.buttons,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -684,4 +705,4 @@ class _IntegrationsPage extends React.Component<IntegrationsProps, IntegrationsS
|
|||
debouncedUpdateIntegrations = debounce(this.applyFilters, FILTERS_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
export const IntegrationsPage = withRouter(withMobXProviderContext(_IntegrationsPage));
|
||||
export const IntegrationsPage = withRouter(withMobXProviderContext(withTheme2(_IntegrationsPage)));
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.header__title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.newWebhookButton {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -48px;
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, ConfirmModal, ConfirmModalProps, HorizontalGroup, Icon, IconButton } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, ConfirmModal, ConfirmModalProps, HorizontalGroup, Icon, IconButton, withTheme2 } from '@grafana/ui';
|
||||
import { observer } from 'mobx-react';
|
||||
import { LegacyNavHeading } from 'navbar/LegacyNavHeading';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { bem, getUtilStyles } from 'styles/utils.styles';
|
||||
|
||||
import { GTable } from 'components/GTable/GTable';
|
||||
import { HamburgerContextMenu } from 'components/HamburgerContextMenu/HamburgerContextMenu';
|
||||
|
|
@ -33,15 +35,11 @@ import { isUserActionAllowed, UserActions } from 'utils/authorization/authorizat
|
|||
import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts';
|
||||
import { openErrorNotification, openNotification } from 'utils/utils';
|
||||
|
||||
import styles from './OutgoingWebhooks.module.scss';
|
||||
import { WebhookFormActionType } from './OutgoingWebhooks.types';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface OutgoingWebhooksProps
|
||||
extends WithStoreProps,
|
||||
PageProps,
|
||||
RouteComponentProps<{ id: string; action: string }> {}
|
||||
interface OutgoingWebhooksProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string; action: string }> {
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
interface OutgoingWebhooksState extends PageBaseState {
|
||||
outgoingWebhookAction?: WebhookFormActionType;
|
||||
|
|
@ -172,6 +170,8 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
},
|
||||
];
|
||||
|
||||
const styles = getStyles();
|
||||
|
||||
return (
|
||||
<PageErrorHandlingWrapper
|
||||
errorData={errorData}
|
||||
|
|
@ -191,7 +191,7 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
}
|
||||
/>
|
||||
)}
|
||||
<div className={cx('newWebhookButton')}>
|
||||
<div className={styles.newWebhookButton}>
|
||||
<PluginLink
|
||||
query={{ page: 'outgoing_webhooks', id: 'new' }}
|
||||
disabled={!isUserActionAllowed(UserActions.OutgoingWebhooksWrite)}
|
||||
|
|
@ -204,13 +204,13 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
</PluginLink>
|
||||
</div>
|
||||
|
||||
<div className={cx('root')} data-testid="outgoing-webhooks-table">
|
||||
<div data-testid="outgoing-webhooks-table">
|
||||
{this.renderOutgoingWebhooksFilters()}
|
||||
<GTable
|
||||
emptyText={webhooks ? 'No outgoing webhooks found' : 'Loading...'}
|
||||
title={() => (
|
||||
<div className={cx('header')}>
|
||||
<div className="header__title">
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerTitle}>
|
||||
<LegacyNavHeading>
|
||||
<Text.Title level={3}>Outgoing Webhooks</Text.Title>
|
||||
</LegacyNavHeading>
|
||||
|
|
@ -244,8 +244,9 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
|
||||
renderOutgoingWebhooksFilters() {
|
||||
const { query, store } = this.props;
|
||||
|
||||
return (
|
||||
<div className={cx('filters')}>
|
||||
<div>
|
||||
<RemoteFilters
|
||||
query={query}
|
||||
page={PAGE.Webhooks}
|
||||
|
|
@ -342,13 +343,16 @@ class OutgoingWebhooks extends React.Component<OutgoingWebhooksProps, OutgoingWe
|
|||
);
|
||||
};
|
||||
|
||||
renderUrl(url: string) {
|
||||
renderUrl = (url: string) => {
|
||||
const { theme } = this.props;
|
||||
const utilStyles = getUtilStyles(theme);
|
||||
|
||||
return (
|
||||
<TextEllipsisTooltip content={url} placement="top">
|
||||
<Text className={cx(TEXT_ELLIPSIS_CLASS, 'line-clamp-3')}>{url}</Text>
|
||||
<Text className={cx(utilStyles.overflowChild, bem(utilStyles.overflowChild, 'line-3'))}>{url}</Text>
|
||||
</TextEllipsisTooltip>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteClick = async (id: ApiSchemas['Webhook']['id']): Promise<void> => {
|
||||
const { store } = this.props;
|
||||
|
|
@ -433,4 +437,26 @@ function convertWebhookUrlToAction(urlAction: string) {
|
|||
|
||||
export { OutgoingWebhooks };
|
||||
|
||||
export const OutgoingWebhooksPage = withRouter(withMobXProviderContext(OutgoingWebhooks));
|
||||
const getStyles = () => {
|
||||
return {
|
||||
header: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-top: 12px;
|
||||
`,
|
||||
|
||||
headerTitle: css`
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
`,
|
||||
|
||||
newWebhookButton: css`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -48px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
export const OutgoingWebhooksPage = withRouter(withMobXProviderContext(withTheme2(OutgoingWebhooks)));
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
.root {
|
||||
--rotations-border: var(--border-weak);
|
||||
--rotations-background: var(--background-secondary);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
column-gap: 8px;
|
||||
row-gap: 8px;
|
||||
min-width: 250px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: sticky; /* TODO check */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.desc {
|
||||
width: 736px;
|
||||
}
|
||||
|
||||
.users-timezones {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rotations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.not-found {
|
||||
margin: 50px auto;
|
||||
text-align: center;
|
||||
}
|
||||
45
grafana-plugin/src/pages/schedule/Schedule.styles.ts
Normal file
45
grafana-plugin/src/pages/schedule/Schedule.styles.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { css } from '@emotion/css';
|
||||
|
||||
export const getScheduleStyles = () => {
|
||||
return {
|
||||
title: css`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
column-gap: 8px;
|
||||
row-gap: 8px;
|
||||
min-width: 250px;
|
||||
align-items: center;
|
||||
`,
|
||||
|
||||
header: css`
|
||||
position: sticky;
|
||||
width: 100%;
|
||||
`,
|
||||
|
||||
desc: css`
|
||||
width: 736px;
|
||||
`,
|
||||
|
||||
usersTimezone: css`
|
||||
width: 100%;
|
||||
`,
|
||||
|
||||
controls: css`
|
||||
width: 100%;
|
||||
`,
|
||||
|
||||
rotations: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`,
|
||||
|
||||
notFound: css`
|
||||
margin: 50px auto;
|
||||
text-align: center;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,7 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, VerticalGroup, IconButton, ToolbarButton, Icon, Modal } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import {
|
||||
Button,
|
||||
HorizontalGroup,
|
||||
VerticalGroup,
|
||||
IconButton,
|
||||
ToolbarButton,
|
||||
Icon,
|
||||
Modal,
|
||||
withTheme2,
|
||||
} from '@grafana/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
|
@ -30,12 +39,11 @@ import { isUserActionAllowed, UserActions } from 'utils/authorization/authorizat
|
|||
import { PLUGIN_ROOT } from 'utils/consts';
|
||||
|
||||
import { getStartOfWeekBasedOnCurrentDate } from './Schedule.helpers';
|
||||
import { getScheduleStyles } from './Schedule.styles';
|
||||
|
||||
import styles from './Schedule.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface SchedulePageProps extends PageProps, WithStoreProps, RouteComponentProps<{ id: string }> {}
|
||||
interface SchedulePageProps extends PageProps, WithStoreProps, RouteComponentProps<{ id: string }> {
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
interface SchedulePageState {
|
||||
schedulePeriodType: string;
|
||||
|
|
@ -121,6 +129,8 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
|
|||
const users = UserHelper.getSearchResult(store.userStore).results;
|
||||
const schedule = scheduleStore.items[scheduleId];
|
||||
|
||||
const styles = getScheduleStyles();
|
||||
|
||||
const disabledRotationForm =
|
||||
!isUserActionAllowed(UserActions.SchedulesWrite) ||
|
||||
schedule?.type !== ScheduleType.API ||
|
||||
|
|
@ -149,9 +159,9 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
|
|||
>
|
||||
{() => (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div>
|
||||
{isNotFoundError ? (
|
||||
<div className={cx('not-found')}>
|
||||
<div className={styles.notFound}>
|
||||
<VerticalGroup spacing="lg" align="center">
|
||||
<Text.Title level={1}>404</Text.Title>
|
||||
<Text.Title level={4}>Schedule not found</Text.Title>
|
||||
|
|
@ -164,11 +174,11 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
|
|||
</div>
|
||||
) : (
|
||||
<VerticalGroup spacing="lg">
|
||||
<div className={cx('header')}>
|
||||
<div className={styles.header}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<div className={cx('title')}>
|
||||
<div className={styles.title}>
|
||||
<PluginLink query={{ page: 'schedules', ...query }}>
|
||||
<IconButton className="button-back" aria-label="Go Back" name="arrow-left" size="xl" />
|
||||
<IconButton aria-label="Go Back" name="arrow-left" size="xl" />
|
||||
</PluginLink>
|
||||
<Text.Title
|
||||
editable={false}
|
||||
|
|
@ -217,7 +227,7 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
|
|||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('users-timezones')}>
|
||||
<div className={styles.usersTimezone}>
|
||||
<UsersTimezones
|
||||
scheduleId={scheduleId}
|
||||
onCallNow={schedule?.on_call_now || []}
|
||||
|
|
@ -229,8 +239,8 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className={cx('rotations')}>
|
||||
<div className={cx('controls')}>
|
||||
<div className={styles.rotations}>
|
||||
<div className={styles.controls}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={this.handleTodayClick}>
|
||||
|
|
@ -501,4 +511,4 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
|
|||
};
|
||||
}
|
||||
|
||||
export const SchedulePage = withRouter(withMobXProviderContext(_SchedulePage));
|
||||
export const SchedulePage = withRouter(withMobXProviderContext(withTheme2(_SchedulePage)));
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
.schedule {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.schedule-personal {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
|
||||
--rotations-border: var(--border-weak);
|
||||
--rotations-background: var(--background-secondary);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: var(--title-marginBottom);
|
||||
}
|
||||
|
||||
.root .buttons {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.schedules__filters-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 4px;
|
||||
column-gap: 8px;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.schedules__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button-back {
|
||||
margin-top: 5px;
|
||||
}
|
||||
44
grafana-plugin/src/pages/schedules/Schedules.styles.ts
Normal file
44
grafana-plugin/src/pages/schedules/Schedules.styles.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { css } from '@emotion/css';
|
||||
|
||||
export const getSchedulesStyles = () => {
|
||||
return {
|
||||
schedule: css`
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
`,
|
||||
|
||||
schedulePersonal: css`
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
`,
|
||||
|
||||
title: css`
|
||||
margin-bottom: 16px;
|
||||
`,
|
||||
|
||||
buttons: css`
|
||||
padding-right: 10px;
|
||||
`,
|
||||
|
||||
rotations: css`
|
||||
position: relative;
|
||||
`,
|
||||
|
||||
schedulesFiltersContainer: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 4px;
|
||||
column-gap: 8px;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
`,
|
||||
|
||||
schedulesActions: css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
gap: 8px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import React, { SyntheticEvent } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, IconButton, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { cx } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, HorizontalGroup, IconButton, LoadingPlaceholder, VerticalGroup, withTheme2 } from '@grafana/ui';
|
||||
import { observer } from 'mobx-react';
|
||||
import qs from 'query-string';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { getUtilStyles } from 'styles/utils.styles';
|
||||
|
||||
import { Avatar } from 'components/Avatar/Avatar';
|
||||
import { NewScheduleSelector } from 'components/NewScheduleSelector/NewScheduleSelector';
|
||||
|
|
@ -31,11 +33,11 @@ import { LocationHelper } from 'utils/LocationHelper';
|
|||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { PAGE, PLUGIN_ROOT, TEXT_ELLIPSIS_CLASS } from 'utils/consts';
|
||||
|
||||
import styles from './Schedules.module.css';
|
||||
import { getSchedulesStyles } from './Schedules.styles';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface SchedulesPageProps extends WithStoreProps, RouteComponentProps, PageProps {}
|
||||
interface SchedulesPageProps extends WithStoreProps, RouteComponentProps, PageProps {
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
interface SchedulesPageState {
|
||||
filters: RemoteFiltersType;
|
||||
|
|
@ -72,14 +74,15 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
|
|||
const { results, count, page_size } = store.scheduleStore.getSearchResult();
|
||||
|
||||
const page = store.filtersStore.currentTablePageNum[PAGE.Schedules];
|
||||
const styles = getSchedulesStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('title')}>
|
||||
<div>
|
||||
<div className={styles.title}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text.Title level={3}>Schedules</Text.Title>
|
||||
<div className={cx('schedules__actions')}>
|
||||
<div className={styles.schedulesActions}>
|
||||
<UserTimezoneSelect onChange={this.refreshExpandedSchedules} />
|
||||
<WithPermissionControlTooltip userAction={UserActions.SchedulesWrite}>
|
||||
<Button variant="primary" onClick={this.handleCreateScheduleClick}>
|
||||
|
|
@ -89,10 +92,10 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
|
|||
</div>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('schedule', 'schedule-personal')}>
|
||||
<div className={cx(styles.schedule, styles.schedulePersonal)}>
|
||||
<SchedulePersonal userPk={store.userStore.currentUserPk} />
|
||||
</div>
|
||||
<div className={cx('schedules__filters-container')}>
|
||||
<div className={styles.schedulesFiltersContainer}>
|
||||
<RemoteFilters
|
||||
query={query}
|
||||
page={PAGE.Schedules}
|
||||
|
|
@ -144,7 +147,7 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
|
|||
|
||||
renderNotFound() {
|
||||
return (
|
||||
<div className={cx('loader')}>
|
||||
<div>
|
||||
<Text type="secondary">Not found</Text>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -177,18 +180,21 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
|
|||
|
||||
refreshExpandedSchedules = () => {
|
||||
const { expandedRowKeys } = this.state;
|
||||
|
||||
expandedRowKeys.forEach(this.props.store.scheduleStore.refreshEvents);
|
||||
};
|
||||
|
||||
renderSchedule = (data: Schedule) => (
|
||||
<div className={cx('schedule')}>
|
||||
<TimelineMarks />
|
||||
<div className={cx('rotations')}>
|
||||
<ScheduleFinal simplified scheduleId={data.id} onSlotClick={this.getScheduleClickHandler(data.id)} />
|
||||
renderSchedule = (data: Schedule) => {
|
||||
const styles = getSchedulesStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.schedule}>
|
||||
<TimelineMarks />
|
||||
<div className={styles.rotations}>
|
||||
<ScheduleFinal simplified scheduleId={data.id} onSlotClick={this.getScheduleClickHandler(data.id)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
getScheduleClickHandler = (scheduleId: Schedule['id']) => {
|
||||
const { history, query } = this.props;
|
||||
|
|
@ -269,6 +275,9 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
|
|||
};
|
||||
|
||||
renderOncallNow = (item: Schedule, _index: number) => {
|
||||
const { theme } = this.props;
|
||||
const utilsStyles = getUtilStyles(theme);
|
||||
|
||||
if (item.on_call_now?.length > 0) {
|
||||
return (
|
||||
<div className="table__email-column">
|
||||
|
|
@ -280,7 +289,7 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
|
|||
<TextEllipsisTooltip placement="top" content={user.username}>
|
||||
<Text type="secondary" className={cx(TEXT_ELLIPSIS_CLASS)}>
|
||||
<Avatar size="small" src={user.avatar} />{' '}
|
||||
<span className={cx('break-word')}>{user.username}</span>
|
||||
<span className={cx(utilsStyles.wordBreakAll)}>{user.username}</span>
|
||||
</Text>
|
||||
</TextEllipsisTooltip>
|
||||
</HorizontalGroup>
|
||||
|
|
@ -393,6 +402,7 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
|
|||
|
||||
getTableColumns = () => {
|
||||
const { grafanaTeamStore } = this.props.store;
|
||||
const styles = getSchedulesStyles();
|
||||
|
||||
return [
|
||||
{
|
||||
|
|
@ -438,10 +448,10 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
|
|||
width: '50px',
|
||||
key: 'buttons',
|
||||
render: this.renderButtons,
|
||||
className: cx('buttons'),
|
||||
className: styles.buttons,
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
export const SchedulesPage = withRouter(withMobXProviderContext(_SchedulesPage));
|
||||
export const SchedulesPage = withRouter(withMobXProviderContext(withTheme2(_SchedulesPage)));
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.tabs__content {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import { AppRootProps } from '@grafana/data';
|
||||
import { Tab, TabsBar } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { Tab, TabsBar, useStyles2 } from '@grafana/ui';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { ChatOpsPage } from 'pages/settings/tabs/ChatOps/ChatOps';
|
||||
|
|
@ -19,10 +19,6 @@ import { CloudPage } from './tabs/Cloud/CloudPage';
|
|||
import LiveSettingsPage from './tabs/LiveSettings/LiveSettingsPage';
|
||||
import { TeamsSettings } from './tabs/TeamsSettings/TeamsSettings';
|
||||
|
||||
import styles from './SettingsPage.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface SettingsPageProps extends AppRootProps, WithStoreProps {}
|
||||
interface SettingsPageState {
|
||||
activeTab: string;
|
||||
|
|
@ -41,7 +37,7 @@ class Settings extends React.Component<SettingsPageProps, SettingsPageState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
return <div className={cx('root')}>{this.renderContent()}</div>;
|
||||
return <div>{this.renderContent()}</div>;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
|
|
@ -134,31 +130,32 @@ interface TabsContentProps {
|
|||
|
||||
const TabsContent = (props: TabsContentProps) => {
|
||||
const { activeTab } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={cx('tabs__content')}>
|
||||
<div className={styles.tabsContent}>
|
||||
{activeTab === SettingsPageTab.MainSettings.key && (
|
||||
<div className={cx('tab__page')}>
|
||||
<div>
|
||||
<MainSettings />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === SettingsPageTab.TeamsSettings.key && (
|
||||
<div className={cx('tab__page')}>
|
||||
<div>
|
||||
<TeamsSettings />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === SettingsPageTab.ChatOps.key && (
|
||||
<div className={cx('tab__page')}>
|
||||
<div>
|
||||
<ChatOpsPage />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === SettingsPageTab.EnvVariables.key && (
|
||||
<div className={cx('tab__page')}>
|
||||
<div>
|
||||
<LiveSettingsPage />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === SettingsPageTab.Cloud.key && (
|
||||
<div className={cx('tab__page')}>
|
||||
<div>
|
||||
<CloudPage />
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -166,4 +163,12 @@ const TabsContent = (props: TabsContentProps) => {
|
|||
);
|
||||
};
|
||||
|
||||
const getStyles = () => {
|
||||
return {
|
||||
tabsContent: css`
|
||||
padding-top: 24px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
export const SettingsPage = withMobXProviderContext(Settings);
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
.users-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.users-filters {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.users-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--title-marginBottom);
|
||||
}
|
||||
|
||||
.users-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.root .users-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.users-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.desc {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.user-filters-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.warning-message-icon {
|
||||
margin-right: 8px;
|
||||
color: var(--warning-text-color);
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
display: inline-block;
|
||||
white-space: break-spaces;
|
||||
line-height: 20px;
|
||||
color: var(--error-text-color);
|
||||
}
|
||||
|
||||
.error-icon svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.warning-message {
|
||||
color: var(--warning-text-color);
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: var(--success-text-color);
|
||||
}
|
||||
74
grafana-plugin/src/pages/users/Users.styles.ts
Normal file
74
grafana-plugin/src/pages/users/Users.styles.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
export const getUsersStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
usersTtitle: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`,
|
||||
|
||||
userAvatar: css`
|
||||
margin-right: 10px;
|
||||
`,
|
||||
|
||||
usersFilters: css`
|
||||
margin-bottom: 20px;
|
||||
`,
|
||||
|
||||
usersHeader: css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
`,
|
||||
|
||||
usersHeaderLeft: css`
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
`,
|
||||
|
||||
usersTitle: css`
|
||||
margin-bottom: 0;
|
||||
`,
|
||||
|
||||
usersFooter: css`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`,
|
||||
|
||||
desc: css`
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
`,
|
||||
|
||||
userFiltersContainer: css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`,
|
||||
|
||||
warningMessageIcon: css`
|
||||
margin-right: 8px;
|
||||
color: ${theme.colors.warning.text};
|
||||
`,
|
||||
|
||||
errorIcon: css`
|
||||
display: inline-block;
|
||||
white-space: break-spaces;
|
||||
line-height: 20px;
|
||||
color: ${theme.colors.error.text};
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
`,
|
||||
|
||||
warningMessage: css`
|
||||
color: ${theme.colors.warning.text};
|
||||
`,
|
||||
|
||||
successMessage: css`
|
||||
color: ${theme.colors.success.text};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Alert, Button, HorizontalGroup, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { cx } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Button, HorizontalGroup, VerticalGroup, withTheme2 } from '@grafana/ui';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
import { LegacyNavHeading } from 'navbar/LegacyNavHeading';
|
||||
|
|
@ -30,13 +31,13 @@ import { UserActions, generateMissingPermissionMessage, isUserActionAllowed } fr
|
|||
import { PAGE, PLUGIN_ROOT } from 'utils/consts';
|
||||
|
||||
import { getUserRowClassNameFn } from './Users.helpers';
|
||||
import { getUsersStyles } from './Users.styles';
|
||||
|
||||
import styles from './Users.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
const DEBOUNCE_MS = 1000;
|
||||
|
||||
interface UsersProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {}
|
||||
interface UsersProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
const REQUIRED_PERMISSION_TO_VIEW_USERS = UserActions.UserSettingsWrite;
|
||||
|
||||
|
|
@ -132,9 +133,11 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
match: {
|
||||
params: { id },
|
||||
},
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const isAuthorizedToViewUsers = isUserActionAllowed(REQUIRED_PERMISSION_TO_VIEW_USERS);
|
||||
const styles = getUsersStyles(theme);
|
||||
|
||||
return (
|
||||
<PageErrorHandlingWrapper
|
||||
|
|
@ -144,9 +147,9 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
itemNotFoundMessage={`User with id=${id} is not found. Please select user from the list.`}
|
||||
>
|
||||
{() => (
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('users-header')}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline' }}>
|
||||
<div>
|
||||
<div className={styles.usersHeader}>
|
||||
<div className={styles.usersHeaderLeft}>
|
||||
<div>
|
||||
<LegacyNavHeading>
|
||||
<Text.Title level={3}>Users</Text.Title>
|
||||
|
|
@ -181,6 +184,7 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
renderContentIfAuthorized(authorizedToViewUsers: boolean) {
|
||||
const {
|
||||
store: { userStore, filtersStore },
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const { usersFilters, userPkToEdit } = this.state;
|
||||
|
|
@ -194,19 +198,20 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
this.setState({ usersFilters: { searchTerm: '' } }, () => {
|
||||
this.updateUsers();
|
||||
});
|
||||
const styles = getUsersStyles(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
{authorizedToViewUsers ? (
|
||||
<>
|
||||
<div className={cx('user-filters-container')} data-testid="users-filters">
|
||||
<div className={styles.userFiltersContainer} data-testid="users-filters">
|
||||
<UsersFilters
|
||||
className={cx('users-filters')}
|
||||
className={styles.usersFilters}
|
||||
value={usersFilters}
|
||||
isLoading={results === undefined}
|
||||
onChange={this.handleUsersFiltersChange}
|
||||
/>
|
||||
<Button variant="secondary" icon="times" onClick={handleClear} className={cx('searchIntegrationClear')}>
|
||||
<Button variant="secondary" icon="times" onClick={handleClear}>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -248,12 +253,14 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
renderTitle = (user: ApiSchemas['User']) => {
|
||||
const {
|
||||
store: { userStore },
|
||||
theme,
|
||||
} = this.props;
|
||||
const isCurrent = userStore.currentUserPk === user.pk;
|
||||
const styles = getUsersStyles(theme);
|
||||
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<Avatar className={cx('user-avatar')} size="large" src={user.avatar} />
|
||||
<Avatar className={styles.userAvatar} size="large" src={user.avatar} />
|
||||
<div
|
||||
className={cx({
|
||||
'current-user': isCurrent,
|
||||
|
|
@ -284,10 +291,10 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
renderContacts = (user: ApiSchemas['User']) => {
|
||||
const { store } = this.props;
|
||||
return (
|
||||
<div className={cx('contacts')}>
|
||||
<div className={cx('contact')}>Slack: {user.slack_user_identity?.name || '-'}</div>
|
||||
<div>
|
||||
<div>Slack: {user.slack_user_identity?.name || '-'}</div>
|
||||
{store.hasFeature(AppFeature.Telegram) && (
|
||||
<div className={cx('contact')}>Telegram: {user.telegram_configuration?.telegram_nick_name || '-'}</div>
|
||||
<div>Telegram: {user.telegram_configuration?.telegram_nick_name || '-'}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -453,4 +460,4 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
};
|
||||
}
|
||||
|
||||
export const UsersPage = withRouter(withMobXProviderContext(Users));
|
||||
export const UsersPage = withRouter(withMobXProviderContext(withTheme2(Users)));
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@ import tinycolor from 'tinycolor2';
|
|||
|
||||
export const getUtilStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
flex: css`
|
||||
display: flex;
|
||||
`,
|
||||
|
||||
width100: css`
|
||||
width: 100%;
|
||||
`,
|
||||
|
||||
loadingPlaceholder: css`
|
||||
margin-bottom: 0;
|
||||
margin-right: 4px;
|
||||
`,
|
||||
|
||||
disabled: css`
|
||||
opacity: 0.5;
|
||||
`,
|
||||
|
|
@ -30,6 +31,48 @@ export const getUtilStyles = (theme: GrafanaTheme2) => {
|
|||
cursorDefault: css`
|
||||
cursor: default;
|
||||
`,
|
||||
|
||||
wordBreakAll: css`
|
||||
word-break: break-all;
|
||||
`,
|
||||
|
||||
...getCommonFlexStyles(),
|
||||
...getCommonOverflowStyles(),
|
||||
};
|
||||
};
|
||||
|
||||
const getCommonOverflowStyles = () => {
|
||||
return {
|
||||
overflowChild: css`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
white-space: initial;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
${[1, 2, 3].map(
|
||||
(num) => `
|
||||
&--line-${num} {
|
||||
-webkit-line-clamp: ${num} !important;
|
||||
}
|
||||
`
|
||||
)}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
const getCommonFlexStyles = () => {
|
||||
return {
|
||||
flex: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`,
|
||||
|
||||
// TODO: auto-generate these incrementally instead (XS, MD, LG etc, simillar to overflow)
|
||||
flexGapXS: css`
|
||||
gap: 4px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -78,4 +121,5 @@ export enum COLORS {
|
|||
GRAY_8 = '#595959',
|
||||
GRAY_9 = '#434343',
|
||||
GREEN_5 = '#6ccf8e',
|
||||
BORDER = 'rgba(204, 204, 220, 0.25)',
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue