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:
Rares Mardare 2024-05-27 16:28:56 +03:00 committed by GitHub
parent d52e821c33
commit f7beced64e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 1674 additions and 1449 deletions

View file

@ -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);

View file

@ -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>

View file

@ -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">

View file

@ -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

View file

@ -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>
)}

View file

@ -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>
);

View file

@ -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'}>

View file

@ -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>
)}

View file

@ -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>
);
};

View file

@ -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 && (
<>

View file

@ -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" />

View file

@ -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 && (

View file

@ -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>
</>
);
}

View file

@ -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"

View file

@ -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>

View file

@ -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'}

View file

@ -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>

View file

@ -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"

View file

@ -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>
)}

View file

@ -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};
`;
}

View file

@ -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>}

View file

@ -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>

View file

@ -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}>

View file

@ -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>

View file

@ -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) => (

View file

@ -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;
}

View file

@ -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;
`,
};
};

View file

@ -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}

View file

@ -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"

View file

@ -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>
);
};

View file

@ -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>

View file

@ -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>
);
};

View file

@ -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>

View file

@ -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;
}

View 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};
}
`,
};
};

View file

@ -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));

View file

@ -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>
)}

View file

@ -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));

View file

@ -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>
)}

View file

@ -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);
}

View file

@ -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};
`,
};
};

View file

@ -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>
);

View file

@ -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;
}

View file

@ -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;
`,
};
};

View file

@ -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)));

View file

@ -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;
`,
};
};

View file

@ -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;
}

View file

@ -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)));

View file

@ -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%;
}
}

View file

@ -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)));

View file

@ -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>
)}

View file

@ -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;
}

View file

@ -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;
`,
};
};

View file

@ -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)));

View file

@ -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;
}

View file

@ -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)));

View file

@ -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;
}

View 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;
`,
};
};

View file

@ -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)));

View file

@ -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;
}

View 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;
`,
};
};

View file

@ -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)));

View file

@ -1,3 +0,0 @@
.tabs__content {
padding-top: 24px;
}

View file

@ -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);

View file

@ -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);
}

View 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};
`,
};
};

View file

@ -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)));

View file

@ -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)',
}