Migrate remaining stylesheets to Emotion (#5022)

Closes https://github.com/grafana/oncall/issues/4201
This commit is contained in:
Rares Mardare 2024-09-18 17:20:46 +03:00 committed by GitHub
parent c7a7a3f81a
commit d8d4a58035
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
197 changed files with 3588 additions and 3812 deletions

View file

@ -1,3 +1,13 @@
.theme-light {
--working-hours-shades-color: rgba(17, 18, 23, 0.15);
--working-hours-shades-color-light: rgba(17, 18, 23, 0.04);
}
.theme-dark:not(.theme-light) {
--working-hours-shades-color: rgba(17, 18, 23, 0.15);
--working-hours-shades-color-light: rgba(17, 18, 23, 0.1);
}
.configure-plugin {
margin-top: 10px;
}

View file

@ -1,229 +0,0 @@
/* -----
* Flex
*/
.u-flex {
display: flex !important;
flex-direction: row;
}
.u-align-items-center {
align-items: center;
}
.u-flex-center {
justify-content: center;
align-items: center;
}
.u-flex-space-between {
justify-content: space-between;
}
.u-flex-grow-1 {
flex-grow: 1;
}
.u-flex-gap-xs {
gap: 4px;
}
/* -----
* Margins/Paddings
*/
.u-margin-right-xs {
margin-right: 4px;
}
.u-margin-left-xs {
margin-left: 4px;
}
.u-margin-bottom-none {
margin-bottom: 0;
}
.u-margin-bottom-md {
margin-bottom: 12px;
}
.u-margin-bottom-xxs {
margin-bottom: 2px;
}
.u-margin-top-xs {
margin-top: 4px;
}
.u-padding-top-md {
padding-top: 12px;
}
.u-padding-top-none {
padding-top: 0;
}
.u-padding-left-lg {
padding-left: 24px;
}
.u-padding-vertical-xs {
padding: 4px 0;
}
.u-pull-right {
margin-left: auto;
}
.u-pull-left {
margin-right: auto;
}
/* -----
* Display
*/
.u-width-100 {
width: 100%;
}
.u-height-100 {
height: 100%;
}
.u-width-height-100 {
width: 100%;
height: 100%;
}
.u-display-block {
display: block;
}
/* -----
* Other
*/
.back-arrow {
padding-top: 8px;
}
.link {
text-decoration: none !important;
}
.u-position-relative {
position: relative;
}
.u-break-word {
word-break: break-word;
}
.u-opacity,
.u-disabled {
opacity: var(--opacity);
}
.u-disabled {
cursor: not-allowed !important;
pointer-events: none;
}
.u-cursor-default {
cursor: default;
}
.thin-line-break {
width: 100%;
border-top: 1px solid var(--always-gray);
margin-top: 8px;
opacity: 15%;
}
.buttons {
padding-bottom: 24px;
}
.loadingPlaceholder {
margin-bottom: 0;
margin-right: 4px;
}
/* -----
* Overflow
*/
.u-overflow-x-auto {
overflow-x: auto;
}
.overflow-child {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
white-space: initial;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.overflow-child--line-1 {
line-clamp: 1;
-webkit-line-clamp: 1;
}
.line-clamp-3 {
line-clamp: 3;
-webkit-line-clamp: 3;
}
.break-word {
word-break: break-all;
}
/* -----
* CSSTransitionGroup fading
*/
.fade-enter {
max-height: 0;
opacity: 0;
}
.fade-enter.fade-enter-active {
max-height: 50px;
opacity: 1;
transition: opacity 300ms ease-in, max-height 300ms ease-in;
}
.fade-leave {
opacity: 1;
max-height: 50px;
}
.fade-leave.fade-leave-active {
max-height: 0;
opacity: 0;
transition: opacity 300ms ease-in, max-height 300ms ease-in;
}
.fade-exit {
opacity: 1;
max-height: 50px;
}
.fade-exit.fade-exit-active {
max-height: 0;
opacity: 0;
transition: opacity 300ms ease-in, max-height 300ms ease-in;
}
/* -----
* Widths
*/
.u-max-width-1000 {
max-width: 1000px;
}

View file

@ -1,74 +0,0 @@
:root {
--green-6: #73d13d;
--gray-9: #434343;
--cyan-1: #e6fffb;
--border-radius: 2px;
--gradient-brandVertical: linear-gradient(0.01deg, #f53e4c -31.2%, #f83 113.07%);
--always-gray: #ccccdc;
--title-marginBottom: 16px;
--opacity: 0.5;
}
.theme-light {
--cards-background: rgba(244, 245, 245);
--highlighted-row-bg: var(--cyan-1);
--primary-background: rgb(255, 255, 255);
--secondary-background: rgb(244, 245, 245);
--border: 1px solid rgba(36, 41, 46, 0.12);
--primary-text-color: rgb(36, 41, 46);
--secondary-text-color: rgba(36, 41, 46, 0.75);
--disabled-text-color: rgba(36, 41, 46, 0.5);
--warning-text-color: #f5b73d;
--success-text-color: #1a7f4b;
--error-text-color: #ff5286;
--primary-text-link: #1f62e0;
--timeline-icon-background: rgba(70, 76, 84, 0);
--oncall-icon-stroke-color: #fff;
--hover-selected: #f4f5f5;
--background-canvas: #f4f5f5;
--background-secondary: #f4f5f5;
--background-disabled: rgba(204, 204, 220, 0.11);
--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);
--button-background: rgba(36, 41, 46, 0.08);
--button-hover-background: rgba(36, 41, 46, 0.15);
--box-background: rgba(244, 245, 245);
--working-hours-shades-color: rgba(17, 18, 23, 0.15);
--working-hours-shades-color-light: rgba(17, 18, 23, 0.04);
}
.theme-dark:not(.theme-light) {
--cards-background: var(--gray-9);
--highlighted-row-bg: var(--gray-9);
--disabled-button-color: hsla(0, 0%, 100%, 0.08);
--primary-background: rgb(24, 27, 31);
--secondary-background: rgb(34, 37, 43);
--border: 1px solid rgba(204, 204, 220, 0.15);
--primary-text-color: rgb(204, 204, 220);
--secondary-text-color: rgba(204, 204, 220, 0.65);
--disabled-text-color: rgba(204, 204, 220, 0.4);
--warning-text-color: #f5b73d;
--success-text-color: #1a7f4b;
--error-text-color: #ff5286;
--primary-text-link: #6e9fff;
--timeline-icon-background: rgba(70, 76, 84, 1);
--timeline-icon-background-resolution-note: rgba(50, 116, 217, 1);
--focused-box-shadow: rgb(17 18 23) 0 0 0 2px, rgb(61 113 217) 0 0 0 4px;
--hover-selected: rgba(204, 204, 220, 0.12);
--hover-selected-hardcoded: #34363d;
--oncall-icon-stroke-color: #181b1f;
--background-canvas: #111217;
--background-secondary: #22252b;
--background-disabled: rgba(204, 204, 220, 0.04);
--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);
--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

@ -1,35 +0,0 @@
.root {
border: var(--border);
width: 100%;
}
.header {
padding: 8px;
cursor: pointer;
display: flex;
align-items: center;
}
.header_with-background {
background: var(--secondary-background);
}
.label {
display: block;
margin-left: 8px;
flex-grow: 1;
}
.content {
padding: 16px;
}
.icon {
color: var(--secondary-text-color);
transform-origin: center;
transition: transform 0.2s;
&--rotated {
transform: rotate(90deg);
}
}

View file

@ -1,5 +1,6 @@
import React, { FC, useCallback, useEffect, useState } from 'react';
import { css } from '@emotion/css';
import { SelectableValue } from '@grafana/data';
import { Button, Icon, Select, Stack } from '@grafana/ui';
import { StackSize } from 'helpers/consts';
@ -31,17 +32,27 @@ export const CursorPagination: FC<CursorPaginationProps> = (props) => {
}, []);
return (
<Stack gap={StackSize.md} justifyContent="flex-end">
<Stack>
<Text type="secondary">Items per list</Text>
<Stack gap={StackSize.xs} justifyContent="flex-end">
<Stack gap={StackSize.xs} alignItems="center">
<Text
type="secondary"
className={css`
width: 120px;
`}
>
Items per list
</Text>
<Select
className={css`
max-width: 75px;
`}
isSearchable={false}
options={itemsPerPageOptions}
value={itemsPerPage}
onChange={onChangeItemsPerPageCallback}
/>
</Stack>
<Stack>
<Stack gap={StackSize.xs} alignItems="center">
<Button
aria-label="previous"
size="sm"

View file

@ -16,7 +16,7 @@ export const getBlockStyles = (theme: GrafanaTheme2) => {
}
&--hover:hover {
background: var(--hover-selected);
background: ${theme.isLight ? theme.colors.background.canvas : theme.colors.action.selected};
}
&--bordered {

View file

@ -1,10 +1,21 @@
import React, { useEffect, useReducer } from 'react';
import { css, cx } from '@emotion/css';
import { SelectableValue } from '@grafana/data';
import { Button, Drawer, Icon, IconButton, Input, RadioButtonGroup, Select, Tooltip, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { GENERIC_ERROR, StackSize } from 'helpers/consts';
import { openErrorNotification, openNotification } from 'helpers/helpers';
import {
Button,
Drawer,
Icon,
IconButton,
Input,
RadioButtonGroup,
Select,
Tooltip,
Stack,
useStyles2,
} from '@grafana/ui';
import { StackSize, GENERIC_ERROR } from 'helpers/consts';
import { openNotification, openErrorNotification } from 'helpers/helpers';
import { observer } from 'mobx-react';
import { GTable } from 'components/GTable/GTable';
@ -15,11 +26,9 @@ import { WithConfirm } from 'components/WithConfirm/WithConfirm';
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
import { ContactPoint } from 'models/alert_receive_channel/alert_receive_channel.types';
import { ApiSchemas } from 'network/oncall-api/api.types';
import styles from 'pages/integration/Integration.module.scss';
import { getIntegrationStyles } from 'pages/integration/Integration.styles';
import { useStore } from 'state/useStore';
const cx = cn.bind(styles);
interface IntegrationContactPointState {
isLoading: boolean;
isDrawerOpen: boolean;
@ -39,6 +48,7 @@ interface IntegrationContactPointState {
export const IntegrationContactPoint: React.FC<{
id: ApiSchemas['AlertReceiveChannel']['id'];
}> = observer(({ id }) => {
const styles = useStyles2(getIntegrationStyles);
const { alertReceiveChannelStore } = useStore();
const contactPoints = alertReceiveChannelStore.connectedContactPoints[id];
const warnings = contactPoints?.filter((cp) => !cp.notificationConnected);
@ -98,22 +108,27 @@ export const IntegrationContactPoint: React.FC<{
<IntegrationBlock
noContent={true}
heading={
<div className={cx('u-flex', 'u-flex-space-between')}>
<div
className={css`
display: flex;
justify-content: space-between;
`}
>
{isDrawerOpen && (
<Drawer scrollableContent title="Connected Contact Points" onClose={closeDrawer} closeOnMaskClick={false}>
<div className={cx('contactpoints__drawer')}>
<div>
<GTable
emptyText={'No contact points'}
className={cx('contactpoints__table')}
className={styles.contactPointsTable}
rowKey="id"
data={contactPoints}
columns={getTableColumns()}
/>
<div className={cx('contactpoints__connect')}>
<div className={styles.contactPointsConnect}>
<Stack direction="column" gap={StackSize.md}>
<div
className={cx('contactpoints__connect-toggler')}
className={styles.contactPointsConnectToggler}
onClick={() => setState({ isConnectOpen: !isConnectOpen })}
>
<Stack justifyContent="space-between">
@ -146,7 +161,12 @@ export const IntegrationContactPoint: React.FC<{
content={'Check the notification policy for the contact point in Grafana Alerting'}
placement={'top'}
>
<div className={cx('u-flex', 'u-flex-gap-xs')}>
<div
className={css`
display: flex;
gap: 4px;
`}
>
{renderExclamationIcon()}
<Text type="primary">{warnings.length} with error</Text>
</div>
@ -233,7 +253,16 @@ export const IntegrationContactPoint: React.FC<{
<Button variant="secondary" onClick={closeDrawer}>
Cancel
</Button>
{isLoading && <Icon name="fa fa-spinner" size="md" className={cx('loadingPlaceholder')} />}
{isLoading && (
<Icon
name="fa fa-spinner"
size="md"
className={css`
margin-bottom: 0;
margin-right: 4px;
`}
/>
)}
</Stack>
</Stack>
);
@ -304,7 +333,7 @@ export const IntegrationContactPoint: React.FC<{
function renderExclamationIcon() {
return (
<div className={cx('icon-exclamation')}>
<div className={cx(styles.iconExclamation)}>
<Icon name="exclamation-triangle" />
</div>
);

View file

@ -1,24 +1,24 @@
import React from 'react';
import { Icon, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { Icon, Stack, useStyles2 } from '@grafana/ui';
import { StackSize } from 'helpers/consts';
import { noop } from 'lodash-es';
import { getUtilStyles } from 'styles/utils.styles';
import { IntegrationInputField } from 'components/IntegrationInputField/IntegrationInputField';
import { IntegrationBlock } from 'components/Integrations/IntegrationBlock';
import { IntegrationTag } from 'components/Integrations/IntegrationTag';
import { Text } from 'components/Text/Text';
import { ApiSchemas } from 'network/oncall-api/api.types';
import styles from 'pages/integration/Integration.module.scss';
import { getIntegrationStyles } from 'pages/integration/Integration.styles';
import { useStore } from 'state/useStore';
const cx = cn.bind(styles);
export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveChannel']['id'] }> = ({ id }) => {
const { alertReceiveChannelStore } = useStore();
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[id];
const hasAlerts = !!alertReceiveChannelCounter?.alerts_count;
const styles = useStyles2(getIntegrationStyles);
const utilStyles = useStyles2(getUtilStyles);
const item = alertReceiveChannelStore.items[id];
const url = item?.integration_url || item?.inbound_email;
@ -39,7 +39,7 @@ export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveCha
noContent={hasAlerts}
toggle={noop}
heading={
<div className={cx('how-to-connect__container')}>
<div className={styles.howToConnectContainer}>
<IntegrationTag>{howToConnectTagName(item?.integration)}</IntegrationTag>
{item?.integration === 'direct_paging' ? (
<>
@ -48,7 +48,7 @@ export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveCha
href="https://grafana.com/docs/oncall/latest/integrations/manual"
target="_blank"
rel="noreferrer"
className={cx('u-pull-right')}
className={utilStyles.pullRight}
>
<Text type="link" size="small">
<Stack>
@ -64,7 +64,7 @@ export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveCha
<IntegrationInputField
value={url}
isMasked
className={cx('integration__input-field')}
className={styles.integrationInputField}
showExternal={!!item?.integration_url}
/>
)}
@ -72,7 +72,7 @@ export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveCha
href="https://grafana.com/docs/oncall/latest/integrations/"
target="_blank"
rel="noreferrer"
className={cx('u-pull-right')}
className={utilStyles.pullRight}
>
<Text type="link" size="small">
<Stack>
@ -102,7 +102,7 @@ export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveCha
<Stack direction="column" justifyContent={'flex-start'} gap={StackSize.xs}>
{!hasAlerts && (
<Stack gap={StackSize.xs}>
<Icon name="fa fa-spinner" size="md" className={cx('loadingPlaceholder')} />
<Icon name="fa fa-spinner" size="md" className={utilStyles.loadingPlaceholder} />
<Text type={'primary'}>No alerts yet</Text> {callToAction()}
</Stack>
)}

View file

@ -1,4 +1,4 @@
export const logoCoors: { [key: string]: { x: number; y: number } } = {
export const logoColors: { [key: string]: { x: number; y: number } } = {
grafana: { x: 9, y: 0 },
grafana_alerting: { x: 9, y: 0 },
legacy_grafana_alerting: { x: 9, y: 0 },

View file

@ -1,39 +0,0 @@
.bg {
background: url(../../assets/img/integration-logos.png);
background-repeat: no-repeat;
}
.bg_ServiceNow {
background: url(../../assets/img/ServiceNow.png);
background-size: 100% !important;
}
.bg_PagerDuty {
background: url(../../assets/img/PagerDuty.png);
background-size: 100% !important;
}
.bg_ElastAlert {
background: url(../../assets/img/ElastAlert.svg);
background-size: 100% !important;
}
.bg_HeartBeatMonitoring {
background: url(../../assets/img/HeartBeatMonitoring.png);
background-size: 100% !important;
}
.bg_GrafanaLegacyAlerting {
background: url(../../assets/img/grafana-legacy-alerting-icon.svg);
background-size: 100% !important;
}
.bg_GrafanaAlerting {
background: url(../../assets/img/grafana_icon.svg);
background-size: 100% !important;
}
.bg_InboundEmail {
background: url(../../assets/img/inbound-email.png);
background-size: 100% !important;
}

View file

@ -1,30 +1,36 @@
import React, { FC } from 'react';
import cn from 'classnames/bind';
import { css, cx } from '@emotion/css';
import { useStyles2 } from '@grafana/ui';
import ElasticAlertIcon from 'assets/img/ElastAlert.svg';
import HeartbeatMonitoringIcon from 'assets/img/HeartBeatMonitoring.png';
import PagerDutyIcon from 'assets/img/PagerDuty.png';
import ServiceNowIcon from 'assets/img/ServiceNow.png';
import GrafanaLegacyAlertingIcon from 'assets/img/grafana-legacy-alerting-icon.svg';
import GrafanaIcon from 'assets/img/grafana_icon.svg';
import InboundEmailIcon from 'assets/img/inbound-email.png';
import IntegrationLogos from 'assets/img/integration-logos.png';
import { logoColors } from 'components/IntegrationLogo/IntegrationLogo.config';
import { SelectOption } from 'state/types';
import { logoCoors } from './IntegrationLogo.config';
import styles from 'components/IntegrationLogo/IntegrationLogo.module.css';
export interface IntegrationLogoProps {
integration: SelectOption;
scale: number;
}
const cx = cn.bind(styles);
const SPRITESHEET_WIDTH = 3000;
const LOGO_WIDTH = 200;
export const IntegrationLogo: FC<IntegrationLogoProps> = (props) => {
const { integration, scale } = props;
const styles = useStyles2(getStyles);
if (!integration) {
return null;
}
const coors = logoCoors[integration.value] || { x: 2, y: 14 };
const coors = logoColors[integration.value] || { x: 2, y: 14 };
const bgStyle = {
backgroundPosition: `-${coors?.x * LOGO_WIDTH * scale}px -${coors?.y * LOGO_WIDTH * scale}px`,
@ -34,13 +40,55 @@ export const IntegrationLogo: FC<IntegrationLogoProps> = (props) => {
};
return (
<div className={cx('root')}>
<div
className={cx('bg', {
[`bg_${integration.display_name.replace(new RegExp(' ', 'g'), '')}`]: true,
})}
style={bgStyle}
/>
</div>
<div
className={cx(styles.bg, {
[styles[`${integration.display_name.replace(new RegExp(' ', 'g'), '')}`]]: true,
})}
style={bgStyle}
/>
);
};
const getStyles = () => {
return {
bg: css`
background: url(${IntegrationLogos});
background-repeat: no-repeat;
`,
bgServiceNow: css`
background: url(${ServiceNowIcon})
background-size: 100% !important;
`,
bgPagerDuty: css`
background: url(${PagerDutyIcon});
background-size: 100% !important;
`,
bgElastAlert: css`
background: url(${ElasticAlertIcon});
background-size: 100% !important;
`,
bgHeartBeatMonitoring: css`
background: url(${HeartbeatMonitoringIcon});
background-size: 100% !important;
`,
bgGrafanaLegacyAlerting: css`
background: url(${GrafanaLegacyAlertingIcon});
background-size: 100% !important;
`,
bgGrafanaAlerting: css`
background: url(${GrafanaIcon});
background-size: 100% !important;
`,
bgInboundEmail: css`
background: url(${InboundEmailIcon});
background-size: 100% !important;
`,
};
};

View file

@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { Button, Icon, Modal, Tooltip, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { Button, Icon, Modal, Tooltip, Stack, useStyles2 } from '@grafana/ui';
import { StackSize } from 'helpers/consts';
import { openNotification } from 'helpers/helpers';
import CopyToClipboard from 'react-copy-to-clipboard';
@ -14,11 +13,9 @@ import { PluginLink } from 'components/PluginLink/PluginLink';
import { Text } from 'components/Text/Text';
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
import { ApiSchemas } from 'network/oncall-api/api.types';
import styles from 'pages/integration/Integration.module.scss';
import { getIntegrationStyles } from 'pages/integration/Integration.styles';
import { useStore } from 'state/useStore';
const cx = cn.bind(styles);
interface IntegrationSendDemoPayloadModalProps {
isOpen: boolean;
alertReceiveChannel: ApiSchemas['AlertReceiveChannel'];
@ -31,6 +28,7 @@ export const IntegrationSendDemoAlertModal: React.FC<IntegrationSendDemoPayloadM
onHideOrCancel,
}) => {
const store = useStore();
const styles = useStyles2(getIntegrationStyles);
const { alertReceiveChannelStore } = store;
const initialDemoJSON = JSON.stringify(alertReceiveChannel.demo_alert_payload, null, 2);
const [demoPayload, setDemoPayload] = useState<string>(initialDemoJSON);
@ -69,7 +67,7 @@ export const IntegrationSendDemoAlertModal: React.FC<IntegrationSendDemoPayloadM
</Tooltip>
</Stack>
<div className={cx('integration__payloadInput')}>
<div className={styles.integrationPayloadInput}>
<MonacoEditor
value={initialDemoJSON}
disabled={true}

View file

@ -66,7 +66,7 @@ const getStyles = (theme: GrafanaTheme2) => {
padding: 15px;
background: ${theme.colors.background.primary};
border: 1px solid ${theme.colors.border.weak};
box-shadow: var(--shadows-z3);
box-shadow: ${theme.shadows.z3};
border-radius: 2px;
z-index: 10;
`,

View file

@ -1,7 +1,7 @@
import React, { ComponentProps, FC, useCallback } from 'react';
import { css, cx } from '@emotion/css';
import { CodeEditor, CodeEditorSuggestionItemKind, LoadingPlaceholder } from '@grafana/ui';
import cn from 'classnames';
import { getPaths } from 'helpers/helpers';
import { conf, language as jinja2Language } from './jinja2';
@ -104,7 +104,13 @@ export const MonacoEditor: FC<MonacoEditorProps> = (props) => {
height={height}
onEditorDidMount={handleMount}
getSuggestions={useAutoCompleteList ? autoCompleteList : undefined}
containerStyles={cn('u-width-height-100', containerClassName)}
containerStyles={cx(
css`
width: 100%;
height: 100%;
`,
containerClassName
)}
{...codeEditorProps}
/>
);

View file

@ -1,11 +0,0 @@
.root {
display: block;
}
.block {
width: 100%;
}
.content {
padding-bottom: 24px;
}

View file

@ -46,7 +46,7 @@ export interface NotificationPolicyProps {
isDisabled: boolean;
}
export class NotificationPolicy extends React.Component<NotificationPolicyProps, any> {
export class _NotificationPolicy extends React.Component<NotificationPolicyProps, any> {
constructor(props: NotificationPolicyProps) {
super(props);
}
@ -336,4 +336,4 @@ const getStyles = (_theme: GrafanaTheme2) => {
};
};
export default SortableElement(withTheme2(NotificationPolicy));
export const NotificationPolicy = SortableElement(withTheme2(_NotificationPolicy));

View file

@ -1,9 +1,11 @@
import React from 'react';
import { SortableContainer } from 'react-sortable-hoc';
import { SortableContainer, SortableContainerProps } from 'react-sortable-hoc';
import { Timeline } from 'components/Timeline/Timeline';
export const SortableList = SortableContainer(({ className, children }: any) => {
export const SortableList = SortableContainer<
SortableContainerProps & { className?: string; children: React.ReactNode[] }
>(({ className, children }: any) => {
return <Timeline className={className}>{children}</Timeline>;
});

View file

@ -1,13 +1,9 @@
import React, { ReactElement, useEffect, useRef, useState } from 'react';
import { cx } from '@emotion/css';
import { Tooltip } from '@grafana/ui';
import cn from 'classnames/bind';
import { TEXT_ELLIPSIS_CLASS } from 'helpers/consts';
import styles from 'assets/style/utils.css';
const cx = cn.bind(styles);
interface TextEllipsisTooltipProps {
content?: string;
queryClassName?: string;
@ -23,7 +19,7 @@ export const TextEllipsisTooltip: React.FC<TextEllipsisTooltipProps> = ({
placement,
children,
}) => {
const [isEllipsis, setIsEllipsis] = useState(true);
const [isEllipsis, setIsEllipsis] = useState(false);
const elContentRef = useRef<HTMLDivElement>(null);
useEffect(() => {

View file

@ -1,33 +0,0 @@
.root {
padding: 0;
margin: 0;
list-style: none;
}
.item {
display: flex;
align-items: center;
margin: 10px 0;
}
.dot {
width: 28px;
height: 28px;
border-radius: 50%;
text-align: center;
line-height: 28px;
font-size: 14px;
font-weight: 400;
color: white;
flex-shrink: 0;
}
.content {
margin: 0 0 0 24px;
word-break: break-word;
flex-grow: 1;
}
.content--noMargin {
margin: 0;
}

View file

@ -0,0 +1,39 @@
import { css } from '@emotion/css';
export const getTimelineStyles = () => {
return {
root: css`
padding: 0;
margin: 0;
list-style: none;
`,
item: css`
display: flex;
align-items: center;
margin: 10px 0;
`,
dot: css`
width: 28px;
height: 28px;
border-radius: 50%;
text-align: center;
line-height: 28px;
font-size: 14px;
font-weight: 400;
color: white;
flex-shrink: 0;
`,
content: css`
margin: 0 0 0 24px;
word-break: break-word;
flex-grow: 1;
`,
contentNoMargin: css`
margin: 0;
`,
};
};

View file

@ -1,13 +1,11 @@
import React from 'react';
import cn from 'classnames/bind';
import { cx } from '@emotion/css';
import { useStyles2 } from '@grafana/ui';
import { getTimelineStyles } from './Timeline.styles';
import { TimelineItem, TimelineItemProps } from './TimelineItem';
import styles from 'components/Timeline/Timeline.module.css';
const cx = cn.bind(styles);
export interface TimelineProps {
className?: string;
children?: any;
@ -19,8 +17,9 @@ interface TimelineType extends React.FC<TimelineProps> {
export const Timeline: TimelineType = (props) => {
const { className, children } = props;
const styles = useStyles2(getTimelineStyles);
return <ul className={cx('root', className)}>{children}</ul>;
return <ul className={cx(styles.root, className)}>{children}</ul>;
};
Timeline.Item = TimelineItem;

View file

@ -1,10 +1,9 @@
import React from 'react';
import cn from 'classnames/bind';
import { cx } from '@emotion/css';
import { useStyles2 } from '@grafana/ui';
import styles from 'components/Timeline/Timeline.module.css';
const cx = cn.bind(styles);
import { getTimelineStyles } from './Timeline.styles';
export interface TimelineItemProps {
className?: string;
@ -28,17 +27,19 @@ export const TimelineItem: React.FC<TimelineItemProps> = ({
textColor = '#ffffff',
number,
}) => {
const styles = useStyles2(getTimelineStyles);
return (
<li className={cx('item', className)}>
<li className={cx(styles.item, className)}>
{!isDisabled && (
<div
className={cx('dot', backgroundClassName || '')}
className={cx(styles.dot, backgroundClassName || '')}
style={{ backgroundColor: backgroundHexNumber || '', color: textColor }}
>
{number}
</div>
)}
<div className={cx('content', contentClassName, { 'content--noMargin': isDisabled })}>{children}</div>
<div className={cx(styles.content, contentClassName, { [styles.contentNoMargin]: isDisabled })}>{children}</div>
</li>
);
};

View file

@ -1,57 +0,0 @@
.root {
display: flex;
align-items: center;
flex-direction: column;
}
.title {
margin-top: 100px;
}
.steps {
margin-top: 100px;
display: flex;
gap: 10px;
margin-bottom: 100px;
align-items: flex-start;
}
.step {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
width: 120px;
text-align: center;
}
.icon {
width: 60px;
height: 60px;
background: var(--secondary-background);
border-radius: 50%;
text-align: center;
line-height: 55px;
}
.icon_active {
border: 2px solid #ffb375;
}
.arrow {
margin-top: 20px;
}
.arrow svg {
fill: #ccccdc;
}
:global(.theme-dark) .arrow svg {
fill-opacity: 0.15;
}
@media (min-width: 1540px) {
.step {
width: 170px;
}
}

View file

@ -10,8 +10,8 @@ export const getUserGroupStyles = (theme: GrafanaTheme2) => {
sortable: css`
z-index: 1062;
box-shadow: var(--focused-box-shadow);
background: var(--hover-selected-hardcoded) !important;
box-shadow: ${theme.isDark ? 'rgb(17 18 23) 0 0 0 2px, rgb(61 113 217) 0 0 0 4px;' : ''};
background: ${theme.isDark ? '#34363d' : ''};
`,
separator: css`

View file

@ -1,63 +0,0 @@
.add-responders-dropdown {
max-height: 500px;
overflow: hidden;
border: var(--border-medium);
position: absolute;
right: 16px;
top: 55px;
background: var(--primary-background);
width: 340px;
z-index: 10;
}
.info-alert {
margin: 8px;
}
.learn-more-link {
display: inline-block;
}
.responder-item {
cursor: pointer;
width: 280px;
overflow: hidden;
}
.responder-name {
word-break: normal;
}
.responder-team {
text-align: right;
}
.responders-filters {
margin: 8px;
}
.radio-buttons {
margin: 8px;
}
.loading-placeholder {
margin: 8px;
}
.table {
max-height: 150px;
overflow: auto;
padding: 4px 0;
& tr:hover {
background: var(--background-secondary) !important;
}
& tbody tr:nth-child(odd) {
background: unset;
}
}
.user-results-section-header {
padding: 10px 8px;
}

View file

@ -0,0 +1,70 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
export const getAddRespondersPopupStyles = (theme: GrafanaTheme2) => {
return {
addRespondersDropdown: css`
max-height: 500px;
overflow: hidden;
border: 1px solid ${theme.colors.border.medium};
position: absolute;
right: 16px;
top: 55px;
background: ${theme.colors.background.primary};
width: 340px;
z-index: 10;
`,
infoAlert: css`
margin: 8px;
`,
learnMoreLink: css`
display: inline-block;
`,
responderItem: css`
cursor: pointer;
width: 280px;
overflow: hidden;
`,
responderName: css`
word-break: normal;
`,
responderTeam: css`
text-align: right;
`,
respondersFilters: css`
margin: 8px;
`,
radioButtons: css`
margin: 8px;
`,
LoadingPlaceholder: css`
margin: 8px;
`,
table: css`
max-height: 150px;
overflow: auto;
padding: 4px 0;
& tr:hover {
background: ${theme.colors.background.secondary} !important;
}
& tbody tr:nth-child(odd) {
background: unset;
}
`,
userResultsSectionHeader: css`
padding: 10px 8px;
`,
};
};

View file

@ -1,9 +1,9 @@
import React, { useState, useCallback, useEffect, useRef, FC } from 'react';
import { Alert, Icon, Input, LoadingPlaceholder, RadioButtonGroup, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { css } from '@emotion/css';
import { Alert, Icon, Input, LoadingPlaceholder, RadioButtonGroup, Stack, useStyles2 } from '@grafana/ui';
import { StackSize } from 'helpers/consts';
import { useDebouncedCallback, useOnClickOutside } from 'helpers/hooks';
import { useOnClickOutside, useDebouncedCallback } from 'helpers/hooks';
import { observer } from 'mobx-react';
import { ColumnsType } from 'rc-table/lib/interface';
@ -15,7 +15,7 @@ import { UserHelper } from 'models/user/user.helpers';
import { ApiSchemas } from 'network/oncall-api/api.types';
import { useStore } from 'state/useStore';
import styles from './AddRespondersPopup.module.scss';
import { getAddRespondersPopupStyles } from './AddRespondersPopup.styles';
type Props = {
mode: 'create' | 'update';
@ -28,8 +28,6 @@ type Props = {
existingPagedUsers?: ApiSchemas['AlertGroup']['paged_users'];
};
const cx = cn.bind(styles);
enum TabOptions {
Teams = 'teams',
Users = 'users',
@ -45,6 +43,7 @@ export const AddRespondersPopup = observer(
setShowUserConfirmationModal,
}: Props) => {
const { directPagingStore, grafanaTeamStore } = useStore();
const styles = useStyles2(getAddRespondersPopupStyles);
const { selectedTeamResponder, selectedUserResponders } = directPagingStore;
const isCreateMode = mode === 'create';
@ -205,7 +204,7 @@ export const AddRespondersPopup = observer(
const { avatar_url, name, number_of_users_currently_oncall } = team;
return (
<div onClick={() => addTeamResponder(team)} className={cx('responder-item')}>
<div onClick={() => addTeamResponder(team)} className={styles.responderItem}>
<Stack justifyContent="space-between">
<Stack>
<Avatar size="small" src={avatar_url} />
@ -233,17 +232,17 @@ export const AddRespondersPopup = observer(
const disabled = userIsSelected(user);
return (
<div onClick={() => (disabled ? undefined : onClickUser(user))} className={cx('responder-item')}>
<div onClick={() => (disabled ? undefined : onClickUser(user))} className={styles.responderItem}>
<Stack justifyContent="space-between">
<Stack>
<Avatar size="small" src={avatar} />
<Text type={disabled ? 'disabled' : undefined} className={cx('responder-name')}>
<Text type={disabled ? 'disabled' : undefined} className={styles.responderName}>
{name || username}
</Text>
</Stack>
{/* TODO: we should add an elippsis and/or tooltip in the event that the user has a ton of teams */}
{teams?.length > 0 && (
<Text type="secondary" className={cx('responder-team')}>
<Text type="secondary" className={styles.responderTeam}>
{teams.map(({ name }) => name).join(', ')}
</Text>
)}
@ -266,7 +265,7 @@ export const AddRespondersPopup = observer(
}) =>
users.length > 0 && (
<>
<Text type="secondary" className={cx('user-results-section-header')}>
<Text type="secondary" className={styles.userResultsSectionHeader}>
{header}
</Text>
<GTable<ApiSchemas['UserIsCurrentlyOnCall']>
@ -274,7 +273,7 @@ export const AddRespondersPopup = observer(
rowKey="pk"
columns={userColumns}
data={users}
className={cx('table')}
className={styles.table}
showHeader={false}
/>
</>
@ -282,11 +281,11 @@ export const AddRespondersPopup = observer(
return (
visible && (
<div data-testid="add-responders-popup" ref={ref} className={cx('add-responders-dropdown')}>
<div data-testid="add-responders-popup" ref={ref} className={styles.addRespondersDropdown}>
<Input
suffix={<Icon name="search" />}
key="search"
className={cx('responders-filters')}
className={styles.respondersFilters}
data-testid="add-responders-search-input"
value={search}
placeholder="Search"
@ -300,13 +299,20 @@ export const AddRespondersPopup = observer(
{ value: TabOptions.Teams, label: 'Teams' },
{ value: TabOptions.Users, label: 'Users' },
]}
className={cx('radio-buttons')}
className={styles.radioButtons}
value={activeOption}
onChange={onChangeTab}
fullWidth
/>
)}
{searchLoading && <LoadingPlaceholder className={cx('loading-placeholder')} text="Loading..." />}
{searchLoading && (
<LoadingPlaceholder
className={css`
margin-bottom: 0;
`}
text="Loading..."
/>
)}
{!searchLoading && activeOption === TabOptions.Teams && (
<>
{selectedTeamResponder ? (
@ -317,14 +323,14 @@ export const AddRespondersPopup = observer(
) : (
<>
<Alert
className={cx('info-alert')}
className={styles.infoAlert}
severity="info"
title={
(
<Text type="primary">
You can only page teams which have a Direct Paging integration that is configured.{' '}
<a
className={cx('learn-more-link')}
className={styles.learnMoreLink}
href="https://grafana.com/docs/oncall/latest/integrations/manual/#set-up-direct-paging-for-a-team"
target="_blank"
rel="noreferrer"
@ -345,7 +351,7 @@ export const AddRespondersPopup = observer(
rowKey="id"
columns={teamColumns}
data={teamSearchResults}
className={cx('table')}
className={styles.table}
showHeader={false}
/>
</>
@ -355,7 +361,7 @@ export const AddRespondersPopup = observer(
{!searchLoading && activeOption === TabOptions.Users && (
<>
<Alert
className={cx('info-alert')}
className={styles.infoAlert}
severity="info"
title={
(

View file

@ -1,15 +1,11 @@
import React, { FC } from 'react';
import { css } from '@emotion/css';
import { SelectableValue } from '@grafana/data';
import { Select, ActionMeta } from '@grafana/ui';
import cn from 'classnames/bind';
import { NotificationPolicyValue } from 'containers/AddResponders/AddResponders.types';
import styles from './NotificationPoliciesSelect.module.scss';
const cx = cn.bind(styles);
type Props = {
disabled?: boolean;
important: boolean;
@ -18,7 +14,9 @@ type Props = {
export const NotificationPoliciesSelect: FC<Props> = ({ disabled = false, important, onChange }) => (
<Select
className={cx('select')}
className={css`
width: 150px !important;
`}
width="auto"
isSearchable={false}
value={Number(important)}

View file

@ -1,7 +0,0 @@
.root {
border-radius: 2px;
background: var(--secondary-background);
padding: 2px 2px 2px 12px;
flex-wrap: wrap;
width: 100%;
}

View file

@ -0,0 +1,51 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
export const getConnectorsStyles = (theme: GrafanaTheme2) => {
return {
root: css`
border-radius: 2px;
background: ${theme.colors.background.secondary};
padding: 2px 2px 2px 12px;
flex-wrap: wrap;
width: 100%;
`,
userItem: css`
margin-bottom: 15px;
`,
userValue: css`
font-size: 16px;
`,
iCalSettings: css`
display: block;
`,
iCalButton: css`
margin-top: 24px;
`,
icalLinkContainer: css`
margin-top: 8px;
`,
icalLink: css`
display: block;
border-radius: 2px;
border: 1px solid ${theme.colors.border.weak};
padding: 4px;
background-color: ${theme.colors.background.secondary};
overflow-wrap: break-word;
`,
warningIcon: css`
color: ${theme.colors.warning.text};
`,
errorMessage: css`
color: ${theme.colors.error.text};
`,
};
};

View file

@ -1,7 +1,7 @@
import React, { useCallback } from 'react';
import { InlineSwitch, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { cx } from '@emotion/css';
import { InlineSwitch, Stack, useStyles2 } from '@grafana/ui';
import { UserActions } from 'helpers/authorization/authorization';
import { StackSize } from 'helpers/consts';
import { observer } from 'mobx-react';
@ -12,9 +12,7 @@ import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import { MSTeamsChannel } from 'models/msteams_channel/msteams_channel.types';
import { useStore } from 'state/useStore';
import styles from 'containers/AlertRules/parts/connectors/Connectors.module.css';
const cx = cn.bind(styles);
import { getConnectorsStyles } from './Connectors.styles';
interface MSTeamsConnectorProps {
channelFilterId: ChannelFilter['id'];
@ -24,6 +22,8 @@ export const MSTeamsConnector = observer((props: MSTeamsConnectorProps) => {
const { channelFilterId } = props;
const store = useStore();
const styles = useStyles2(getConnectorsStyles);
const {
alertReceiveChannelStore,
msteamsChannelStore,
@ -48,9 +48,9 @@ export const MSTeamsConnector = observer((props: MSTeamsConnectorProps) => {
}, []);
return (
<div className={cx('root')}>
<div className={styles.root}>
<Stack wrap="wrap" gap={StackSize.sm}>
<div className={cx('slack-channel-switch')}>
<div>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<InlineSwitch
value={channelFilter.notification_backends?.MSTEAMS?.enabled}

View file

@ -1,7 +1,7 @@
import React, { useCallback } from 'react';
import { InlineSwitch, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { cx } from '@emotion/css';
import { InlineSwitch, Stack, useStyles2 } from '@grafana/ui';
import { UserActions } from 'helpers/authorization/authorization';
import { StackSize } from 'helpers/consts';
import { observer } from 'mobx-react';
@ -13,9 +13,7 @@ import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config'
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
import { useStore } from 'state/useStore';
import styles from './Connectors.module.css';
const cx = cn.bind(styles);
import { getConnectorsStyles } from './Connectors.styles';
interface SlackConnectorProps {
channelFilterId: ChannelFilter['id'];
@ -25,6 +23,8 @@ export const SlackConnector = observer((props: SlackConnectorProps) => {
const { channelFilterId } = props;
const store = useStore();
const styles = useStyles2(getConnectorsStyles);
const {
organizationStore: { currentOrganization },
alertReceiveChannelStore,
@ -45,9 +45,9 @@ export const SlackConnector = observer((props: SlackConnectorProps) => {
}, []);
return (
<div className={cx('root')}>
<div className={styles.root}>
<Stack wrap="wrap" gap={StackSize.sm}>
<div className={cx('slack-channel-switch')}>
<div>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<InlineSwitch
value={channelFilter.notify_in_slack}

View file

@ -1,7 +1,7 @@
import React, { useCallback } from 'react';
import { InlineSwitch, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { cx } from '@emotion/css';
import { InlineSwitch, Stack, useStyles2 } from '@grafana/ui';
import { UserActions } from 'helpers/authorization/authorization';
import { StackSize } from 'helpers/consts';
import { observer } from 'mobx-react';
@ -12,9 +12,7 @@ import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import { TelegramChannel } from 'models/telegram_channel/telegram_channel.types';
import { useStore } from 'state/useStore';
import styles from './Connectors.module.css';
const cx = cn.bind(styles);
import { getConnectorsStyles } from './Connectors.styles';
interface TelegramConnectorProps {
channelFilterId: ChannelFilter['id'];
@ -22,6 +20,8 @@ interface TelegramConnectorProps {
export const TelegramConnector = observer(({ channelFilterId }: TelegramConnectorProps) => {
const store = useStore();
const styles = useStyles2(getConnectorsStyles);
const {
alertReceiveChannelStore,
telegramChannelStore,
@ -40,9 +40,9 @@ export const TelegramConnector = observer(({ channelFilterId }: TelegramConnecto
}, []);
return (
<div className={cx('root')}>
<div className={styles.root}>
<Stack wrap="wrap" gap={StackSize.sm}>
<div className={cx('slack-channel-switch')}>
<div>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<InlineSwitch
value={channelFilter.notify_in_telegram}

View file

@ -1,29 +0,0 @@
.alerts-container {
display: flex;
flex-direction: column;
margin-bottom: 10px;
gap: 10px;
&--legacy {
// legacy navbar requires different padding-top
padding-top: 10px;
}
&:empty {
display: none;
}
}
.alert {
margin: 0;
}
.instructions-link {
color: var(--primary-text-link);
}
@media (max-width: 768px) {
.alerts-container--legacy {
padding-top: 50px;
}
}

View file

@ -1,7 +1,8 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Alert } from '@grafana/ui';
import cn from 'classnames/bind';
import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, useStyles2 } from '@grafana/ui';
import { sanitize } from 'dompurify';
import { LocationHelper } from 'helpers/LocationHelper';
import { isUserActionAllowed, UserActions } from 'helpers/authorization/authorization';
@ -18,12 +19,8 @@ import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
import { AppFeature } from 'state/features';
import { useStore } from 'state/useStore';
import styles from './Alerts.module.scss';
import plugin from '../../../package.json'; // eslint-disable-line
const cx = cn.bind(styles);
enum AlertID {
CONNECTIVITY_WARNING = 'Connectivity Warning',
USER_GOOGLE_OAUTH2_TOKEN_MISSING_SCOPES = 'User Google OAuth2 token is missing scopes',
@ -32,6 +29,7 @@ enum AlertID {
export const Alerts = observer(() => {
const queryParams = useQueryParams();
const [showSlackInstallAlert, setShowSlackInstallAlert] = useState<SlackError | undefined>();
const styles = useStyles2(getStyles);
const forceUpdate = useForceUpdate();
@ -78,10 +76,10 @@ export const Alerts = observer(() => {
return null;
}
return (
<div className={cx('alerts-container', { 'alerts-container--legacy': !isTopNavbar() })}>
<div className={cx(styles.alertsContainer, { [styles.alertsContainerLegacy]: !isTopNavbar() })}>
{showSlackInstallAlert && (
<Alert
className={cx('alert')}
className={styles.alert}
onRemove={handleCloseInstallSlackAlert}
severity="error"
title="Slack integration error"
@ -91,7 +89,7 @@ export const Alerts = observer(() => {
)}
{showCurrentUserGoogleOAuth2TokenIsMissingScopes() && (
<Alert
className={cx('alert')}
className={styles.alert}
severity="warning"
title="User Google OAuth2 token is missing scopes"
onRemove={getRemoveAlertHandler(AlertID.USER_GOOGLE_OAUTH2_TOKEN_MISSING_SCOPES)}
@ -107,7 +105,7 @@ export const Alerts = observer(() => {
)}
{showBannerTeam() && (
<Alert
className={cx('alert')}
className={styles.alert}
severity="success"
title={currentOrganization.banner.title}
onRemove={getRemoveAlertHandler(currentOrganization?.banner.title)}
@ -121,7 +119,7 @@ export const Alerts = observer(() => {
)}
{showMismatchWarning() && (
<Alert
className={cx('alert')}
className={styles.alert}
severity="warning"
title={'Version mismatch!'}
onRemove={getRemoveAlertHandler(versionMismatchLocalStorageId)}
@ -136,7 +134,7 @@ export const Alerts = observer(() => {
href={'https://grafana.com/docs/oncall/latest/open-source/#update-grafana-oncall-oss'}
target="_blank"
rel="noreferrer"
className={cx('instructions-link')}
className={styles.instructionsLink}
>
the update instructions
</a>
@ -146,7 +144,7 @@ export const Alerts = observer(() => {
{showChannelWarnings() && (
<Alert
onRemove={getRemoveAlertHandler(AlertID.CONNECTIVITY_WARNING)}
className={cx('alert')}
className={styles.alert}
severity="warning"
title="Notification Warning! Possible notification miss."
>
@ -211,3 +209,34 @@ export const Alerts = observer(() => {
);
}
});
const getStyles = (theme: GrafanaTheme2) => {
return {
alertsContainer: css`
display: flex;
flex-direction: column;
margin-bottom: 10px;
gap: 10px;
'&:empty': {
display: none;
}
`,
alert: css`
margin: 0;
`,
instructionsLink: css`
color: ${theme.colors.primary.text};
`,
alertsContainerLegacy: css`
paddingtop: '10px';
@media (max-width: 768px) {
padding-top: 50px;
}
`,
};
};

View file

@ -1,18 +0,0 @@
.token__inputContainer {
width: 100%;
display: flex;
}
.token__input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.token__copyButton {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.field {
flex-grow: 1;
}

View file

@ -0,0 +1,21 @@
import { css } from '@emotion/css';
export const getApiTokenFormStyles = () => {
return {
tokenInputContainer: css`
width: 100%;
display: flex;
`,
tokenInput: css`
border-top-right-radius: 0;
border-bottom-right-radius: 0;
`,
tokenCopyButton: css`
border-top-left-radius: 0;
border-bottom-left-radius: 0;
`,
field: css`
flex-grow: 1;
`,
};
};

View file

@ -1,8 +1,7 @@
import React, { HTMLAttributes, useState } from 'react';
import { Button, Field, Input, Label, Modal, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { openErrorNotification, openNotification } from 'helpers/helpers';
import { Button, Field, Input, Label, Modal, Stack, useStyles2 } from '@grafana/ui';
import { openNotification, openErrorNotification } from 'helpers/helpers';
import { get } from 'lodash-es';
import { observer } from 'mobx-react';
import CopyToClipboard from 'react-copy-to-clipboard';
@ -12,9 +11,7 @@ import { RenderConditionally } from 'components/RenderConditionally/RenderCondit
import { SourceCode } from 'components/SourceCode/SourceCode';
import { useStore } from 'state/useStore';
import styles from './ApiTokenForm.module.css';
const cx = cn.bind(styles);
import { getApiTokenFormStyles } from './ApiTokenForm.styles';
interface TokenCreationModalProps extends HTMLAttributes<HTMLElement> {
visible: boolean;
@ -29,6 +26,7 @@ interface FormFields {
export const ApiTokenForm = observer((props: TokenCreationModalProps) => {
const { onHide = () => {}, onUpdate = () => {} } = props;
const [token, setToken] = useState('');
const styles = useStyles2(getApiTokenFormStyles);
const store = useStore();
const formMethods = useForm<FormFields>({
@ -50,7 +48,7 @@ export const ApiTokenForm = observer((props: TokenCreationModalProps) => {
<form onSubmit={handleSubmit(onCreateTokenCallback)}>
<Stack direction="column">
<Label>Token Name</Label>
<div className={cx('token__inputContainer')}>
<div className={styles.tokenInputContainer}>
{renderTokenInput()}
{renderCopyToClipboard()}
</div>
@ -81,14 +79,14 @@ export const ApiTokenForm = observer((props: TokenCreationModalProps) => {
control={control}
rules={{ required: 'Token name is required' }}
render={({ field }) => (
<Field invalid={Boolean(errors['name'])} error={errors['name']?.message} className={cx('field')}>
<Field invalid={Boolean(errors['name'])} error={errors['name']?.message} className={styles.field}>
<>
{token ? (
<Input {...field} disabled={!!token} className={cx('token__input')} />
<Input {...field} disabled={!!token} className={styles.tokenInput} />
) : (
<Input
{...field}
className={cx('token__input')}
className={styles.tokenInput}
maxLength={50}
placeholder="Enter token name"
autoFocus
@ -107,7 +105,7 @@ export const ApiTokenForm = observer((props: TokenCreationModalProps) => {
}
return (
<CopyToClipboard text={token} onCopy={() => openNotification('Token copied')}>
<Button className={cx('token__copyButton')}>Copy Token</Button>
<Button className={styles.tokenCopyButton}>Copy Token</Button>
</CopyToClipboard>
);
}
@ -137,6 +135,6 @@ export const ApiTokenForm = observer((props: TokenCreationModalProps) => {
}
});
function getCurlExample(token, onCallApiUrl) {
function getCurlExample(token: string, onCallApiUrl: string) {
return `curl -H "Authorization: ${token}" ${onCallApiUrl}/api/v1/integrations`;
}

View file

@ -1,13 +0,0 @@
.form {
margin: 20px 0;
}
.incident-matcher {
margin: 20px 0;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}

View file

@ -1,11 +1,11 @@
import React from 'react';
import { css } from '@emotion/css';
import { Button, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import {
generateMissingPermissionMessage,
isUserActionAllowed,
UserActions,
isUserActionAllowed,
generateMissingPermissionMessage,
} from 'helpers/authorization/authorization';
import { observer } from 'mobx-react';
import moment from 'moment-timezone';
@ -20,10 +20,6 @@ import { withMobXProviderContext } from 'state/withStore';
import { ApiTokenForm } from './ApiTokenForm';
import styles from './ApiTokenSettings.module.css';
const cx = cn.bind(styles);
const MAX_TOKENS_PER_USER = 5;
const REQUIRED_PERMISSION_TO_VIEW = UserActions.APIKeysWrite;
@ -81,11 +77,13 @@ class _ApiTokenSettings extends React.Component<ApiTokensProps, any> {
emptyText = 'No tokens found';
}
const styles = getStyles();
return (
<>
<GTable
title={() => (
<div className={cx('header')}>
<div className={styles.header}>
<Stack alignItems="flex-end">
<Text.Title level={3}>API Tokens</Text.Title>
</Stack>
@ -161,4 +159,14 @@ class _ApiTokenSettings extends React.Component<ApiTokensProps, any> {
};
}
const getStyles = () => {
return {
header: css`
display: flex;
justify-content: space-between;
align-items: center;
`,
};
};
export const ApiTokenSettings = withMobXProviderContext(_ApiTokenSettings);

View file

@ -1,3 +0,0 @@
.root {
display: block;
}

View file

@ -1,8 +1,8 @@
import React, { useCallback, useState } from 'react';
import { css, cx } from '@emotion/css';
import { SelectableValue } from '@grafana/data';
import { Button, Field, Icon, Modal, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { UserActions } from 'helpers/authorization/authorization';
import { observer } from 'mobx-react';
import moment from 'moment-timezone';
@ -14,10 +14,6 @@ import { AlertGroupHelper } from 'models/alertgroup/alertgroup.helpers';
import { ApiSchemas } from 'network/oncall-api/api.types';
import { useStore } from 'state/useStore';
import styles from './AttachIncidentForm.module.css';
const cx = cn.bind(styles);
interface AttachIncidentFormProps {
id: ApiSchemas['AlertGroup']['pk'];
onUpdate: () => void;
@ -70,7 +66,9 @@ export const AttachIncidentForm = observer(({ id, onUpdate, onHide }: AttachInci
<Text.Title level={4}>Attach to another alert group</Text.Title>
</Stack>
}
className={cx('root')}
className={css`
display: block;
`}
onDismiss={onHide}
>
<Field

View file

@ -1,8 +1,8 @@
import React, { useMemo, useState } from 'react';
import { css } from '@emotion/css';
import { LabelTag } from '@grafana/labels';
import { Button, Checkbox, IconButton, Input, LoadingPlaceholder, Modal, Stack, useStyles2 } from '@grafana/ui';
import cn from 'classnames/bind';
import { UserActions } from 'helpers/authorization/authorization';
import { PROCESSING_REQUEST_ERROR, StackSize } from 'helpers/consts';
import { WrapWithGlobalNotification } from 'helpers/decorators';
@ -10,7 +10,6 @@ import { pluralize } from 'helpers/helpers';
import { useDebouncedCallback, useIsLoading } from 'helpers/hooks';
import { observer } from 'mobx-react';
import styles from 'assets/style/utils.css';
import { Block } from 'components/GBlock/Block';
import { Text } from 'components/Text/Text';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
@ -23,8 +22,6 @@ import { useStore } from 'state/useStore';
import { getColumnsSelectorWrapperStyles } from './ColumnsSelectorWrapper.styles';
const cx = cn.bind(styles);
interface ColumnsModalProps {
isModalOpen: boolean;
labelKeys: Array<ApiSchemas['LabelKey']>;
@ -40,6 +37,11 @@ interface SearchResult extends Pick<components['schemas']['LabelKey'], 'id' | 'n
const DEBOUNCE_MS = 300;
const loadingPlaceholderCSS = css`
margin-bottom: 0;
margin-right: 4px;
`;
export const ColumnsModal: React.FC<ColumnsModalProps> = observer(
({ isModalOpen, labelKeys, setIsModalOpen, inputRef }) => {
const store = useStore();
@ -108,7 +110,7 @@ export const ColumnsModal: React.FC<ColumnsModalProps> = observer(
{!result.isCollapsed && (
<Block bordered withBackground fullWidth className={styles.valuesBlock}>
{result.values === undefined ? (
<LoadingPlaceholder text="Loading..." className={cx('loadingPlaceholder')} />
<LoadingPlaceholder text="Loading..." className={loadingPlaceholderCSS} />
) : (
renderLabelValues(result.name, result.values)
)}
@ -138,7 +140,7 @@ export const ColumnsModal: React.FC<ColumnsModalProps> = observer(
failure: PROCESSING_REQUEST_ERROR,
})}
>
{isLoading ? <LoadingPlaceholder className={cx('loadingPlaceholder')} text="Loading..." /> : 'Add'}
{isLoading ? <LoadingPlaceholder className={loadingPlaceholderCSS} text="Loading..." /> : 'Add'}
</Button>
</WithPermissionControlTooltip>
</Stack>

View file

@ -1,14 +0,0 @@
.root {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
/* --- GRAFANA UI TUNINGS --- */
.root :global(.filter-table) td {
white-space: break-spaces;
line-height: 20px;
height: auto;
}

View file

@ -1,18 +1,14 @@
import React, { FC, ReactElement } from 'react';
import { NavModelItem } from '@grafana/data';
import { css, cx } from '@emotion/css';
import { AppRootProps, NavModelItem } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { PluginPage } from 'PluginPage';
import { AppRootProps } from 'app-types';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import { Alerts } from 'containers/Alerts/Alerts';
import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
import styles from './DefaultPageLayout.module.scss';
const cx = cn.bind(styles);
interface DefaultPageLayoutProps extends AppRootProps {
children?: any;
page: string;
@ -21,6 +17,7 @@ interface DefaultPageLayoutProps extends AppRootProps {
export const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) => {
const { children, page, pageNav } = props;
const styles = useStyles2(getStyles);
if (isTopNavbar()) {
return renderTopNavbar();
@ -31,7 +28,7 @@ export const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) =>
function renderTopNavbar(): ReactElement {
return (
<PluginPage page={page} pageNav={pageNav as any}>
<div className={cx('root')}>{children}</div>
<div className={styles.root}>{children}</div>
</PluginPage>
);
}
@ -39,8 +36,15 @@ export const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) =>
function renderLegacyNavbar(): ReactElement {
return (
<PluginPage page={page}>
<div className="page-container u-height-100">
<div className={cx('root', 'navbar-legacy')}>
<div
className={cx(
'page-container',
css`
height: 100%;
`
)}
>
<div className={cx(styles.root)}>
<Alerts />
{children}
</div>
@ -49,3 +53,20 @@ export const DefaultPageLayout: FC<DefaultPageLayoutProps> = observer((props) =>
);
}
});
const getStyles = () => {
return {
root: css`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.filter-table td {
white-space: break-spaces;
line-height: 20px;
height: auto;
}
`,
};
};

View file

@ -1,11 +0,0 @@
.regexp-template-code {
width: 100%;
}
.regexp-template-code-error {
border: var(--error-text-color) 1px solid;
}
.regexp-template-editor-modal {
width: 700px;
}

View file

@ -1,7 +1,8 @@
import React, { useState, useCallback } from 'react';
import { Stack, Modal, Tooltip, Icon, Button } from '@grafana/ui';
import cn from 'classnames/bind';
import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Stack, Modal, Tooltip, Icon, Button, useStyles2 } from '@grafana/ui';
import { StackSize } from 'helpers/consts';
import { openErrorNotification } from 'helpers/helpers';
import { debounce } from 'lodash-es';
@ -16,10 +17,6 @@ import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import { ApiSchemas } from 'network/oncall-api/api.types';
import { useStore } from 'state/useStore';
import styles from './EditRegexpRouteTemplateModal.module.css';
const cx = cn.bind(styles);
interface EditRegexpRouteTemplateModalProps {
channelFilterId: ChannelFilter['id'];
template?: TemplateForEdit;
@ -32,6 +29,7 @@ interface EditRegexpRouteTemplateModalProps {
export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemplateModalProps) => {
const { onHide, onUpdateRoute, channelFilterId, onOpenEditIntegrationTemplate, alertReceiveChannelId } = props;
const store = useStore();
const styles = useStyles2(getStyles);
const regexpBody = store.alertReceiveChannelStore.channelFilters[channelFilterId]?.filtering_term;
@ -77,7 +75,7 @@ export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemp
isOpen
onDismiss={onHide}
title="Edit regular expression template"
className={cx('regexp-template-editor-modal')}
className={styles.regexTemplateEditorModal}
>
<Stack direction="column" gap={StackSize.lg}>
<Stack direction="column" gap={StackSize.xs}>
@ -91,7 +89,7 @@ export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemp
</Tooltip>
</Stack>
<div className={cx('regexp-template-code', { 'regexp-template-code-error': showErrorTemplate })}>
<div className={cx(styles.regexTemplateCode, { [styles.regexTemplateCodeError]: showErrorTemplate })}>
<MonacoEditor
value={regexpTemplateBody}
height={'200px'}
@ -124,3 +122,19 @@ export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemp
</Modal>
);
});
const getStyles = (theme: GrafanaTheme2) => {
return {
regexTemplateCode: css`
width: 100%;
`,
regexTemplateCodeError: css`
border: 1px solid ${theme.colors.error.text};
`,
regexTemplateEditorModal: css`
width: 700px;
`,
};
};

View file

@ -1,7 +0,0 @@
.root {
display: block;
}
.icon {
color: var(--success-text-color);
}

View file

@ -1,7 +1,8 @@
import React from 'react';
import { Stack, Badge } from '@grafana/ui';
import cn from 'classnames/bind';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Stack, Badge, useStyles2 } from '@grafana/ui';
import { StackSize } from 'helpers/consts';
import { observer } from 'mobx-react';
@ -10,10 +11,6 @@ import { TeamName } from 'containers/TeamName/TeamName';
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
import { useStore } from 'state/useStore';
import styles from './EscalationChainCard.module.css';
const cx = cn.bind(styles);
interface AlertReceiveChannelCardProps {
id: EscalationChain['id'];
}
@ -22,13 +19,14 @@ export const EscalationChainCard = observer((props: AlertReceiveChannelCardProps
const { id } = props;
const store = useStore();
const styles = useStyles2(getStyles);
const { escalationChainStore, grafanaTeamStore } = store;
const escalationChain = escalationChainStore.items[id];
return (
<div className={cx('root')}>
<div className={styles.root}>
<Stack alignItems="flex-start">
<Stack direction="column" gap={StackSize.xs}>
<Stack gap={StackSize.sm}>
@ -57,3 +55,14 @@ export const EscalationChainCard = observer((props: AlertReceiveChannelCardProps
</div>
);
});
const getStyles = (theme: GrafanaTheme2) => {
return {
root: css`
display: block;
`,
icon: css`
color: ${theme.colors.success.text};
`,
};
};

View file

@ -1,3 +0,0 @@
.root {
display: block;
}

View file

@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { css, cx } from '@emotion/css';
import { Button, Field, Input, Modal, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { openWarningNotification } from 'helpers/helpers';
import { observer } from 'mobx-react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
@ -11,8 +11,6 @@ import { EscalationChain } from 'models/escalation_chain/escalation_chain.types'
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
import { useStore } from 'state/useStore';
import styles from 'containers/EscalationChainForm/EscalationChainForm.module.css';
export enum EscalationChainFormMode {
Create = 'Create',
Copy = 'Copy',
@ -31,8 +29,6 @@ interface EscalationFormFields {
name: string;
}
const cx = cn.bind(styles);
export const EscalationChainForm: FC<EscalationChainFormProps> = observer((props) => {
const { escalationChainId, onHide, onSubmit: onSubmitProp, mode } = props;
@ -68,7 +64,11 @@ export const EscalationChainForm: FC<EscalationChainFormProps> = observer((props
return (
<Modal isOpen title={`${mode} Escalation Chain`} onDismiss={onHide}>
<div className={cx('root')}>
<div
className={css`
display: block;
`}
>
<FormProvider {...formMethods}>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller

View file

@ -1,3 +0,0 @@
.root {
display: block;
}

View file

@ -3,7 +3,6 @@ import React, { ReactElement, useCallback, useEffect } from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { LoadingPlaceholder, Select, useStyles2, useTheme2 } from '@grafana/ui';
import cn from 'classnames/bind';
import { UserActions } from 'helpers/authorization/authorization';
import { observer } from 'mobx-react';
import { getLabelBackgroundTextColorObject } from 'styles/utils.styles';
@ -16,10 +15,6 @@ import { EscalationChain } from 'models/escalation_chain/escalation_chain.types'
import { EscalationPolicyOption } from 'models/escalation_policy/escalation_policy.types';
import { useStore } from 'state/useStore';
import styles from './EscalationChainSteps.module.css';
const cx = cn.bind(styles);
interface EscalationChainStepsProps {
id: EscalationChain['id'];
isDisabled?: boolean;
@ -75,8 +70,7 @@ export const EscalationChainSteps = observer((props: EscalationChainStepsProps)
const { bgColor: successBgColor, textColor: successTextColor } = getLabelBackgroundTextColorObject('green', theme);
return (
// @ts-ignore
<SortableList useDragHandle className={cx('steps')} axis="y" lockAxis="y" onSortEnd={handleSortEnd}>
<SortableList useDragHandle axis="y" lockAxis="y" onSortEnd={handleSortEnd}>
{addonBefore}
{escalationPolicyIds ? (
escalationPolicyIds.map((escalationPolicyId, index) => {

View file

@ -1,8 +0,0 @@
.root {
min-width: 200px;
& > div {
// If not set then inner div will not benefit of min-width
min-width: 200px;
}
}

View file

@ -1,16 +1,12 @@
import React, { ReactElement, useCallback, useEffect } from 'react';
import { cx, css } from '@emotion/css';
import { SelectableValue } from '@grafana/data';
import { AsyncMultiSelect, AsyncSelect } from '@grafana/ui';
import cn from 'classnames/bind';
import { AsyncMultiSelect, AsyncSelect, useStyles2 } from '@grafana/ui';
import { useDebouncedCallback } from 'helpers/hooks';
import { get, isNil } from 'lodash-es';
import { observer } from 'mobx-react';
import styles from './GSelect.module.scss';
const cx = cn.bind(styles);
interface GSelectProps<Item> {
items: {
[key: string]: Item;
@ -75,6 +71,8 @@ export const GSelect = observer(<Item,>(props: GSelectProps<Item>) => {
dataTestId = null,
} = props;
const styles = useStyles2(getGSelectStyles);
const onChangeCallback = useCallback(
(option) => {
if (isMulti) {
@ -152,7 +150,7 @@ export const GSelect = observer(<Item,>(props: GSelectProps<Item>) => {
const Tag = isMulti ? AsyncMultiSelect : AsyncSelect;
return (
<div className={cx('root', className)} data-testid={dataTestId}>
<div className={cx(styles.root, className)} data-testid={dataTestId}>
<Tag
autoFocus={autoFocus}
isSearchable
@ -178,3 +176,16 @@ export const GSelect = observer(<Item,>(props: GSelectProps<Item>) => {
</div>
);
});
const getGSelectStyles = () => {
return {
root: css`
min-width: 200px;
& > div {
// If not set then inner div will not benefit of min-width
min-width: 200px;
}
`,
};
};

View file

@ -1,15 +0,0 @@
.root {
width: 400px;
}
.teamSelectLabel {
display: flex;
}
.teamSelectLink {
color: var(--primary-text-link);
}
.teamSelectInfo {
margin-left: 4px;
}

View file

@ -1,7 +1,8 @@
import React, { useCallback, useState } from 'react';
import { Button, Icon, Label, Modal, Tooltip, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Icon, Label, Modal, Tooltip, Stack, useStyles2 } from '@grafana/ui';
import { UserActions } from 'helpers/authorization/authorization';
import { observer } from 'mobx-react';
@ -10,10 +11,6 @@ import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/W
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
import { useStore } from 'state/useStore';
import styles from './GrafanaTeamSelect.module.scss';
const cx = cn.bind(styles);
interface GrafanaTeamSelectProps {
onSelect: (id: GrafanaTeam['id']) => void;
onHide?: () => void;
@ -24,6 +21,7 @@ interface GrafanaTeamSelectProps {
export const GrafanaTeamSelect = observer(
({ onSelect, onHide, withoutModal, defaultValue }: GrafanaTeamSelectProps) => {
const store = useStore();
const styles = useStyles2(getTeamStyles);
const {
userStore,
@ -76,19 +74,19 @@ export const GrafanaTeamSelect = observer(
}
return (
<Modal onDismiss={onHide} closeOnEscape isOpen title="Select team" className={cx('root')}>
<Modal onDismiss={onHide} closeOnEscape isOpen title="Select team" className={styles.root}>
<Stack direction="column">
<Label>
<span className={cx('teamSelectText')}>
<span>
Select team{''}
<Tooltip content="It will also update your default team">
<Icon name="info-circle" size="md" className={cx('teamSelectInfo')}></Icon>
<Icon name="info-circle" size="md" className={styles.teamSelectInfo}></Icon>
</Tooltip>
</span>
</Label>
<div className={cx('teamSelect')}>{select}</div>
<div>{select}</div>
<WithPermissionControlTooltip userAction={UserActions.TeamsWrite}>
<a href="/org/teams" className={cx('teamSelectLink')}>
<a href="/org/teams" className={styles.teamSelectLink}>
Edit teams
</a>
</WithPermissionControlTooltip>
@ -102,3 +100,23 @@ export const GrafanaTeamSelect = observer(
);
}
);
const getTeamStyles = (theme: GrafanaTheme2) => {
return {
root: css`
width: 400px;
`,
teamSelectLabel: css`
display: flex;
`,
teamSelectLink: css`
color: ${theme.colors.text.primary};
`,
teamSelectInfo: css`
margin-left: 4px;
`,
};
};

View file

@ -1,44 +0,0 @@
.heading-container {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
overflow: hidden;
gap: 12px;
&__item {
display: flex;
white-space: nowrap;
flex-direction: row;
gap: 8px;
}
&__item--large {
flex-grow: 1;
overflow: hidden;
}
&__text {
overflow: hidden;
max-width: calc(100% - 48px);
text-overflow: ellipsis;
}
}
.icon {
margin-right: 4px;
}
.collapsedRoute {
&__container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
&__item {
display: flex;
flex-direction: row;
}
}

View file

@ -1,24 +1,22 @@
import React, { useMemo, useState } from 'react';
import { ConfirmModal, Icon, IconName, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { css, cx } from '@emotion/css';
import { ConfirmModal, Icon, IconName, Stack, useStyles2 } from '@grafana/ui';
import { StackSize } from 'helpers/consts';
import { observer } from 'mobx-react';
import { IntegrationBlock } from 'components/Integrations/IntegrationBlock';
import { PluginLink } from 'components/PluginLink/PluginLink';
import { Text } from 'components/Text/Text';
import styles from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay.module.scss';
import { RouteButtonsDisplay } from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay';
import { RouteHeading } from 'containers/IntegrationContainers/RouteHeading';
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import { ApiSchemas } from 'network/oncall-api/api.types';
import { CommonIntegrationHelper } from 'pages/integration/CommonIntegration.helper';
import { IntegrationHelper } from 'pages/integration/Integration.helper';
import { getIntegrationStyles } from 'pages/integration/Integration.styles';
import { useStore } from 'state/useStore';
const cx = cn.bind(styles);
interface CollapsedIntegrationRouteDisplayProps {
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'];
channelFilterId: ChannelFilter['id'];
@ -42,6 +40,9 @@ export const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRout
onItemMove,
}) => {
const store = useStore();
const styles = useStyles2(getStyles);
const integrationStyles = useStyles2(getIntegrationStyles);
const { escalationChainStore, alertReceiveChannelStore } = store;
const [routeIdForDeletion, setRouteIdForDeletion] = useState<ChannelFilter['id']>(undefined);
@ -70,16 +71,16 @@ export const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRout
key={channelFilterId}
toggle={toggle}
heading={
<div className={cx('heading-container')}>
<div className={styles.headingContainer}>
<RouteHeading
className={cx('heading-container__item', 'heading-container__item--large')}
className={cx(styles.headingContainerItem, styles.headingContainerItemLarge)}
routeWording={routeWording}
routeIndex={routeIndex}
channelFilter={channelFilter}
channelFilterIds={alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId]}
/>
<div className={cx('heading-container__item')}>
<div className={styles.headingContainerItem}>
<RouteButtonsDisplay
alertReceiveChannelId={alertReceiveChannelId}
channelFilterId={channelFilterId}
@ -93,9 +94,9 @@ export const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRout
}
content={
<div>
<div className={cx('collapsedRoute__container')}>
<div className={styles.collapsedRouteContainer}>
{chatOpsAvailableChannels.length > 0 && (
<div className={cx('collapsedRoute__item')}>
<div className={styles.collapsedRouteItem}>
<Stack gap={StackSize.xs}>
<Text type="secondary">Publish to ChatOps</Text>
@ -103,9 +104,15 @@ export const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRout
(chatOpsChannel: { name: string; icon: IconName }, chatOpsIndex) => (
<div
key={`${chatOpsChannel.name}-${chatOpsIndex}`}
className={cx({ 'u-margin-right-xs': chatOpsIndex !== chatOpsAvailableChannels.length })}
className={
chatOpsIndex === chatOpsAvailableChannels.length
? ''
: css`
margin-right: 4px;
`
}
>
<Icon name={chatOpsChannel.icon} className={cx('icon')} />
<Icon name={chatOpsChannel.icon} className={styles.icon} />
<Text type="primary">{chatOpsChannel.name}</Text>
</div>
)
@ -114,27 +121,40 @@ export const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRout
</div>
)}
<div className={cx('collapsedRoute__item')}>
<div className={cx('u-flex', 'u-align-items-center', 'u-flex-gap-xs')}>
<div className={styles.collapsedRouteItem}>
<div
className={css`
display: flex;
align-items: center;
gap: 4px;
`}
>
<Icon name="list-ui-alt" />
<Text type="secondary" className={cx('u-margin-right-xs')}>
<Text
type="secondary"
className={css`
margin-right: 4px;
`}
>
Trigger escalation chain
</Text>
</div>
{escalationChain?.name && (
<PluginLink
className={cx('hover-button')}
target="_blank"
query={{ page: 'escalations', id: channelFilter.escalation_chain }}
>
<PluginLink target="_blank" query={{ page: 'escalations', id: channelFilter.escalation_chain }}>
<Text type="primary">{escalationChain?.name}</Text>
</PluginLink>
)}
{!escalationChain?.name && (
<div className={cx('u-flex', 'u-align-items-center', 'u-flex-gap-xs')}>
<div className={cx('icon-exclamation')}>
<div
className={css`
display: flex;
align-items: center;
gap: 4px;
`}
>
<div className={integrationStyles.iconExclamation}>
<Icon name="exclamation-triangle" />
</div>
<Text type="primary" data-testid="integration-escalation-chain-not-selected">
@ -175,3 +195,50 @@ export const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRout
}
}
);
const getStyles = () => {
return {
headingContainer: css`
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
overflow: hidden;
gap: 12px;
`,
headingContainerItem: css`
display: flex;
white-space: nowrap;
flex-direction: row;
gap: 8px;
`,
headingContainerItemLarge: css`
flex-grow: 1;
overflow: hidden;
`,
headingContainerText: css`
overflow: hidden;
max-width: calc(100% - 48px);
text-overflow: ellipsis;
`,
icon: css`
margin-right: 4px;
`,
collapsedRouteContainer: css`
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
`,
collapsedRouteItem: css`
display: flex;
flex-direction: row;
`,
};
};

View file

@ -1,92 +0,0 @@
.input {
border: var(--border-weak);
&--align {
width: 728px;
}
}
.field {
margin-bottom: 0;
}
.routing-alert {
width: 765px;
}
.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);
}
}
.routing-template-container {
margin-bottom: 8px;
}
.adjust-element-padding {
padding-top: 6px;
}
.default-route-view {
min-height: 40px;
}
.block {
width: 100%;
background-color: var(--background-secondary);
border: var(--border-medium) !important;
}
.labels-panel {
display: flex;
width: 100%;
justify-content: space-between;
}
.heading-container {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
overflow: hidden;
gap: 12px;
&__item {
display: flex;
white-space: nowrap;
flex-direction: row;
gap: 8px;
}
&__item--large {
flex-grow: 1;
overflow: hidden;
}
&__text {
overflow: hidden;
max-width: calc(100% - 48px);
text-overflow: ellipsis;
}
}

View file

@ -0,0 +1,100 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Colors } from 'styles/utils.styles';
export const getExpandedIntegrationRouteDisplayStyles = (theme: GrafanaTheme2) => {
return {
input: css`
border: 1px solid ${theme.colors.border.weak};
`,
inputAlign: css`
width: 728px;
`,
fields: css`
margin-bottom: 0;
`,
routingAlert: css`
width: 765px;
`,
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 ? Colors.HOVER : Colors.GRAY_9};
}
`,
routingTemplateContainer: css`
margin-bottom: 8px;
`,
adjustElementPadding: css`
padding-top: 6px;
`,
defaultRouteView: css`
min-height: 40px;
`,
block: css`
width: 100%;
background-color: ${theme.colors.background.secondary};
border: 1px solid ${theme.colors.border.medium} !important;
`,
labelsPanel: css`
display: flex;
width: 100%;
justify-content: space-between;
`,
headingContainer: css`
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
overflow: hidden;
gap: 12px;
`,
headingContainerItem: css`
display: flex;
white-space: nowrap;
flex-direction: row;
gap: 8px;
`,
headingContainerItemLarge: css`
flex-grow: 1;
overflow: hidden;
`,
headingContainerItemText: css`
overflow: hidden;
max-width: calc(100% - 48px);
text-overflow: ellipsis;
`,
};
};

View file

@ -1,5 +1,6 @@
import React, { useEffect, useReducer, useState } from 'react';
import { css, cx } from '@emotion/css';
import { SelectableValue } from '@grafana/data';
import {
Button,
@ -11,13 +12,14 @@ import {
Select,
RadioButtonGroup,
Alert,
useStyles2,
} from '@grafana/ui';
import cn from 'classnames/bind';
import { UserActions } from 'helpers/authorization/authorization';
import { StackSize } from 'helpers/consts';
import { openNotification } from 'helpers/helpers';
import { observer } from 'mobx-react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { getUtilStyles } from 'styles/utils.styles';
import { CollapsibleTreeView, CollapsibleItem } from 'components/CollapsibleTreeView/CollapsibleTreeView';
import { HamburgerMenuIcon } from 'components/HamburgerMenuIcon/HamburgerMenuIcon';
@ -30,7 +32,6 @@ import { Text } from 'components/Text/Text';
import { WithContextMenu } from 'components/WithContextMenu/WithContextMenu';
import { ChatOpsConnectors } from 'containers/AlertRules/AlertRules';
import { EscalationChainSteps } from 'containers/EscalationChainSteps/EscalationChainSteps';
import styles from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay.module.scss';
import { RouteHeading } from 'containers/IntegrationContainers/RouteHeading';
import { RouteLabelsDisplay } from 'containers/RouteLabelsDisplay/RouteLabelsDisplay';
import { TeamName } from 'containers/TeamName/TeamName';
@ -46,7 +47,7 @@ import { MONACO_INPUT_HEIGHT_SMALL } from 'pages/integration/IntegrationCommon.c
import { AppFeature } from 'state/features';
import { useStore } from 'state/useStore';
const cx = cn.bind(styles);
import { getExpandedIntegrationRouteDisplayStyles } from './ExpandedIntegrationRouteDisplay.styles';
interface ExpandedIntegrationRouteDisplayProps {
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'];
@ -107,6 +108,7 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
const [routingOption, setRoutingOption] = useState<string>(undefined);
const [labels, setLabels] = useState<Array<components['schemas']['LabelPair']>>([]);
const [labelErrors, setLabelErrors] = useState([]);
const styles = useStyles2(getExpandedIntegrationRouteDisplayStyles);
const [{ isEscalationCollapsed, isRefreshingEscalationChains, routeIdForDeletion }, setState] = useReducer(
(state: ExpandedIntegrationRouteDisplayState, newState: Partial<ExpandedIntegrationRouteDisplayState>) => ({
@ -170,9 +172,9 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
collapsedView: null,
canHoverIcon: false,
expandedView: () => (
<div className={cx('adjust-element-padding')}>
<div className={styles.adjustElementPadding}>
{isDefault ? (
<div className={cx('default-route-view')}>
<div className={styles.defaultRouteView}>
<Text customTag="h6" type="primary">
All unmatched alerts are directed to this route, grouped using the Grouping Template, sent to
messengers, and trigger the escalation chain
@ -186,7 +188,7 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
<RenderConditionally shouldRender={hasLabels}>
<Stack direction="column">
<div className={cx('labels-panel')}>
<div className={styles.labelsPanel}>
<RadioButtonGroup
options={QueryBuilderOptions}
value={routingOption}
@ -218,7 +220,7 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
<RenderConditionally shouldRender={routingOption === RoutingOption.TEMPLATE || !hasLabels}>
<Stack direction="column">
<Stack gap={StackSize.xs}>
<div className={cx('input', 'input--align')}>
<div className={cx(styles.input, styles.inputAlign)}>
<MonacoEditor
value={channelFilterTemplate}
disabled={true}
@ -260,7 +262,7 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
collapsedView: null,
canHoverIcon: false,
expandedView: () => (
<div className={cx('adjust-element-padding')}>
<div className={styles.adjustElementPadding}>
<Stack direction="column" gap={StackSize.sm}>
<Text customTag="h6" type="primary">
Publish to ChatOps
@ -278,7 +280,7 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
collapsedView: null,
canHoverIcon: false,
expandedView: () => (
<div className={cx('adjust-element-padding')}>
<div className={styles.adjustElementPadding}>
<Stack direction="column" gap={StackSize.sm}>
<Text customTag="h6" type="primary">
Trigger escalation chain
@ -322,7 +324,7 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
<Button variant={'secondary'} icon={'sync'} size={'md'} onClick={onEscalationChainsRefresh} />
</Tooltip>
<PluginLink className={cx('hover-button')} target="_blank" query={escalationChainRedirectObj}>
<PluginLink target="_blank" query={escalationChainRedirectObj}>
<Tooltip
placement={'top'}
content={channelFilter.escalation_chain ? 'Edit escalation chain' : 'Add an escalation chain'}
@ -365,16 +367,16 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
noContent={false}
key={channelFilterId}
heading={
<div className={cx('heading-container')}>
<div className={styles.headingContainer}>
<RouteHeading
className={cx('heading-container__item', 'heading-container__item--large')}
className={cx(styles.headingContainerItem, styles.headingContainerItemLarge)}
routeWording={routeWording}
routeIndex={routeIndex}
channelFilter={channelFilter}
channelFilterIds={alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId]}
/>
<div className={cx('heading-container__item')}>
<div className={styles.headingContainerItem}>
<RouteButtonsDisplay
alertReceiveChannelId={alertReceiveChannelId}
channelFilterId={channelFilterId}
@ -504,6 +506,8 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
const { alertReceiveChannelStore } = useStore();
const channelFilter = alertReceiveChannelStore.channelFilters[channelFilterId];
const channelFilterIds = alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId];
const styles = useStyles2(getExpandedIntegrationRouteDisplayStyles);
const utilStyles = useStyles2(getUtilStyles);
return (
<Stack gap={StackSize.xs}>
@ -526,13 +530,13 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
{!channelFilter.is_default && (
<WithContextMenu
renderMenuItems={() => (
<div className={cx('integrations-actionsList')}>
<div className={cx('integrations-actionItem')} onClick={openRouteTemplateEditor}>
<div className={styles.integrationsActionsList}>
<div className={styles.integrationsActionItem} onClick={openRouteTemplateEditor}>
<Text type="primary">Edit Template</Text>
</div>
<CopyToClipboard text={channelFilter.id} onCopy={() => openNotification('Route ID is copied')}>
<div className={cx('integrations-actionItem')}>
<div className={cx(styles.integrationsActionItem)}>
<Stack gap={StackSize.xs}>
<Icon name="copy" />
@ -541,10 +545,10 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
</div>
</CopyToClipboard>
<div className={cx('thin-line-break')} />
<div className={cx(utilStyles.thinLineBreak)} />
<WithPermissionControlTooltip key="delete" userAction={UserActions.IntegrationsWrite}>
<div className={cx('integrations-actionItem')} onClick={onDelete}>
<div className={styles.integrationsActionItem} onClick={onDelete}>
<Text type="danger">
<Stack gap={StackSize.xs}>
<Icon name="trash-alt" />
@ -561,7 +565,11 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
openMenu={openMenu}
listBorder={2}
listWidth={200}
className={'hamburgerMenu--small'}
className={css`
height: 24px;
width: 22px;
cursor: pointer;
`}
stopPropagation={true}
/>
)}

View file

@ -1,8 +0,0 @@
.instruction {
ol,
ul {
padding: 0;
margin: 0;
list-style: none;
}
}

View file

@ -1,8 +1,8 @@
import React, { ReactElement, useEffect, useState } from 'react';
import { css, cx } from '@emotion/css';
import { SelectableValue } from '@grafana/data';
import { Button, Drawer, Field, Icon, Select, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { UserActions } from 'helpers/authorization/authorization';
import { StackSize } from 'helpers/consts';
import { openNotification } from 'helpers/helpers';
@ -17,10 +17,6 @@ import { SelectOption } from 'state/types';
import { useStore } from 'state/useStore';
import { withMobXProviderContext } from 'state/withStore';
import styles from './IntegrationHeartbeatForm.module.scss';
const cx = cn.bind(styles);
interface IntegrationHeartbeatFormProps {
alertReceveChannelId: ApiSchemas['AlertReceiveChannel']['id'];
onClose?: () => void;
@ -56,7 +52,11 @@ const _IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: I
</Text>
<Stack direction="column" gap={StackSize.md}>
<div className={cx('u-width-100')}>
<div
className={css`
width: 100%;
`}
>
<Field label={'Setup heartbeat interval'}>
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<Select
@ -73,7 +73,11 @@ const _IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: I
</WithPermissionControlTooltip>
</Field>
</div>
<div className={cx('u-width-100')}>
<div
className={css`
width: 100%;
`}
>
<Field label="Endpoint" description="Use the following unique Grafana link to send GET and POST requests">
<IntegrationInputField value={heartbeat?.link} showEye={false} isMasked={false} />
</Field>

View file

@ -1,8 +1,8 @@
import React, { useState, useCallback } from 'react';
import { InlineSwitch, Tooltip } from '@grafana/ui';
import cn from 'classnames/bind';
import { openErrorNotification, openNotification } from 'helpers/helpers';
import { cx } from '@emotion/css';
import { InlineSwitch, Tooltip, useStyles2 } from '@grafana/ui';
import { openNotification, openErrorNotification } from 'helpers/helpers';
import { observer } from 'mobx-react';
import { IntegrationBlockItem } from 'components/Integrations/IntegrationBlockItem';
@ -14,12 +14,10 @@ import { getTemplatesToRender } from 'containers/IntegrationContainers/Integrati
import { AlertTemplatesDTO } from 'models/alert_templates/alert_templates';
import { ApiSchemas } from 'network/oncall-api/api.types';
import { IntegrationHelper } from 'pages/integration/Integration.helper';
import styles from 'pages/integration/Integration.module.scss';
import { getIntegrationStyles } from 'pages/integration/Integration.styles';
import { MONACO_INPUT_HEIGHT_TALL } from 'pages/integration/IntegrationCommon.config';
import { useStore } from 'state/useStore';
const cx = cn.bind(styles);
interface IntegrationTemplateListProps {
templates: AlertTemplatesDTO[];
alertReceiveChannelId: ApiSchemas['AlertReceiveChannel']['id'];
@ -40,6 +38,7 @@ export const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = o
const [isRestoringTemplate, setIsRestoringTemplate] = useState(false);
const [templateRestoreName, setTemplateRestoreName] = useState<string>(undefined);
const [autoresolveValue, setAutoresolveValue] = useState(alertReceiveChannelAllowSourceBasedResolving);
const styles = useStyles2(getIntegrationStyles);
const handleSaveClick = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
setAutoresolveValue(event.target.checked);
@ -52,7 +51,7 @@ export const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = o
const templatesToRender = getTemplatesToRender(features);
return (
<div className={cx('integration__templates')}>
<div>
{templatesToRender.map((template, key) => (
<IntegrationBlockItem key={key}>
<VerticalBlock>
@ -80,14 +79,16 @@ export const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = o
<InlineSwitch
value={autoresolveValue}
onChange={handleSaveClick}
className={cx('inline-switch')}
className={styles.inlineSwitch}
transparent
/>
</Tooltip>
)}
{isTemplateEditable(contents.name) && (
<div
className={cx('input', { 'input-with-toggle': isResolveConditionTemplate(contents.name) })}
className={cx(styles.input, {
[styles.inputWithToggler]: isResolveConditionTemplate(contents.name),
})}
>
<MonacoEditor
value={IntegrationHelper.getFilteredTemplate(
@ -144,5 +145,6 @@ export const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = o
);
const VerticalBlock: React.FC<{ children: any[] }> = ({ children }) => {
return <div className={cx('vertical-block')}>{children}</div>;
const styles = useStyles2(getIntegrationStyles);
return <div className={styles.verticalBlock}>{children}</div>;
};

View file

@ -1,63 +0,0 @@
.form {
width: 100%;
}
.extra-fields {
padding: 12px;
margin-bottom: 24px;
border: var(--border-weak);
border-radius: var(--border-radius);
&__radio {
margin-bottom: 12px;
}
&__icon {
margin-top: -4px;
}
}
.selectors-container {
width: 100%;
display: flex;
flex-direction: column;
margin-bottom: -15px;
}
.textarea:hover {
// TODO: change this to fetch from emotion instead
}
.collapse {
width: 100%;
margin-bottom: 24px;
}
.collapse svg {
color: var(--primary-text-link) !important;
}
.integration-info-list {
list-style-position: inside;
margin: 16px 0;
}
.integration-info-item {
margin-left: 16px;
}
.servicenow-heading {
margin-bottom: 16px;
}
.webhook-test {
margin-bottom: 16px;
}
.webhook-switch {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
margin-bottom: 24px;
}

View file

@ -1,74 +0,0 @@
.content {
margin: 4px 4px 50px 4px;
padding-bottom: 24px;
}
.cards {
display: flex;
flex-wrap: wrap;
gap: 24px;
overflow: auto;
scroll-snap-type: y mandatory;
width: 100%;
}
.cards_centered {
justify-content: center;
align-items: center;
}
.card {
width: 48%;
height: 88px;
scroll-snap-align: start;
scroll-snap-stop: normal;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
cursor: pointer;
position: relative;
gap: 20px;
}
.card_featured {
width: 100%;
height: 106px;
}
.title {
margin: 10px 0 10px 0;
max-width: 500px;
}
.footer {
display: block;
margin-top: 10px;
}
.search-integration {
width: 100%;
margin-bottom: 24px;
}
.extra-fields {
padding: 12px;
margin-bottom: 24px;
border: var(--border-weak);
border-radius: var(--border-radius);
&__radio {
margin-bottom: 12px;
}
&__icon {
margin-top: -4px;
}
}
.selectors-container {
width: 100%;
display: flex;
flex-direction: column;
margin-bottom: -15px;
}

View file

@ -0,0 +1,82 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
export const getIntegrationFormContainerStyles = (theme: GrafanaTheme2) => {
return {
content: css`
margin: 4px 4px 50px 4px;
padding-bottom: 24px;
`,
cards: css`
display: flex;
flex-wrap: wrap;
gap: 24px;
overflow: auto;
scroll-snap-type: y mandatory;
width: 100%;
`,
cardsCentered: css`
justify-content: center;
align-items: center;
`,
card: css`
width: 48%;
height: 88px;
scroll-snap-align: start;
scroll-snap-stop: normal;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
cursor: pointer;
position: relative;
gap: 20px;
`,
cardFeatured: css`
width: 100%;
height: 106px;
cursor: pointer;
`,
title: css`
margin: 10px 0 10px 0;
max-width: 500px;
`,
footer: css`
display: block;
margin-top: 10px;
`,
searchIntegration: css`
width: 100%;
margin-bottom: 24px;
`,
extraFields: css`
padding: 12px;
margin-bottom: 24px;
border: 1px solid ${theme.colors.border.weak};
border-radius: 2px;
`,
extraFieldsRadio: css`
margin-bottom: 12px;
`,
extraFieldsIcon: css`
margin-top: -4px;
`,
selectorsContainer: css`
width: 100%;
display: flex;
flex-direction: column;
margin-bottom: -15px;
`,
};
};

View file

@ -1,7 +1,7 @@
import React, { useState, ChangeEvent } from 'react';
import { Drawer, Stack, Input, Tag, EmptySearchResult } from '@grafana/ui';
import cn from 'classnames/bind';
import { cx } from '@emotion/css';
import { Drawer, Stack, Input, Tag, EmptySearchResult, useStyles2 } from '@grafana/ui';
import { StackSize } from 'helpers/consts';
import { observer } from 'mobx-react';
@ -12,9 +12,7 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
import { useStore } from 'state/useStore';
import { IntegrationForm } from './IntegrationForm';
import styles from './IntegrationFormContainer.module.scss';
const cx = cn.bind(styles);
import { getIntegrationFormContainerStyles } from './IntegrationFormContainer.styles';
interface IntegrationFormContainerProps {
id: ApiSchemas['AlertReceiveChannel']['id'] | 'new';
@ -31,6 +29,8 @@ export const IntegrationFormContainer = observer((props: IntegrationFormContaine
const { alertReceiveChannelStore } = store;
const [filterValue, setFilterValue] = useState('');
const styles = useStyles2(getIntegrationFormContainerStyles);
const [showNewIntegrationForm, setShowNewIntegrationForm] = useState(false);
const [selectedOption, setSelectedOption] = useState<ApiSchemas['AlertReceiveChannelIntegrationOptions']>(undefined);
const [showIntegrationsListDrawer, setshowIntegrationsListDrawer] = useState(id === 'new');
@ -59,14 +59,14 @@ export const IntegrationFormContainer = observer((props: IntegrationFormContaine
<>
{showIntegrationsListDrawer && (
<Drawer scrollableContent title="New Integration" onClose={onHide} closeOnMaskClick={false} width="640px">
<div className={cx('content')}>
<div className={styles.content}>
<Stack direction="column">
<Text type="secondary">
Integration receives alerts on an unique API URL, interprets them using set of templates tailored for
monitoring system and starts escalations.
</Text>
<div className={cx('search-integration')}>
<div className={styles.searchIntegration}>
<Input
autoFocus
value={filterValue}
@ -82,7 +82,7 @@ export const IntegrationFormContainer = observer((props: IntegrationFormContaine
)}
{(showNewIntegrationForm || !showIntegrationsListDrawer) && (
<Drawer scrollableContent title={getTitle()} onClose={onHide} closeOnMaskClick={false} width="640px">
<div className={cx('content')}>
<div className={styles.content}>
<Stack direction="column">
<IntegrationForm
id={id}
@ -123,8 +123,10 @@ const IntegrationBlocks: React.FC<{
options: Array<ApiSchemas['AlertReceiveChannelIntegrationOptions']>;
onBlockClick: (option: ApiSchemas['AlertReceiveChannelIntegrationOptions']) => void;
}> = ({ options, onBlockClick }) => {
const styles = useStyles2(getIntegrationFormContainerStyles);
return (
<div className={cx('cards')} data-testid="create-integration-modal">
<div className={styles.cards} data-testid="create-integration-modal">
{options.length ? (
options.map((alertReceiveChannelChoice) => {
return (
@ -134,12 +136,11 @@ const IntegrationBlocks: React.FC<{
shadowed
onClick={() => onBlockClick(alertReceiveChannelChoice)}
key={alertReceiveChannelChoice.value}
className={cx('card', { card_featured: alertReceiveChannelChoice.featured })}
className={cx(styles.card, { [styles.cardFeatured]: alertReceiveChannelChoice.featured })}
>
<div className={cx('card-bg')}>
<IntegrationLogo integration={alertReceiveChannelChoice} scale={0.2} />
</div>
<div className={cx('title')}>
<IntegrationLogo integration={alertReceiveChannelChoice} scale={0.2} />
<div className={styles.title}>
<Stack direction="column" gap={alertReceiveChannelChoice.featured ? StackSize.xs : StackSize.none}>
<Stack>
<Text strong data-testid="integration-display-name">

View file

@ -1,14 +0,0 @@
.labels-list {
margin: 0;
list-style-type: none;
> li {
margin: 10px 0;
}
}
.buttons {
width: 100%;
margin-top: 30px;
margin-bottom: 24px;
}

View file

@ -1,9 +1,9 @@
import React, { ChangeEvent, useState } from 'react';
import { css } from '@emotion/css';
import { ServiceLabels } from '@grafana/labels';
import { Alert, Button, Drawer, Dropdown, InlineSwitch, Input, Menu, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { DOCS_ROOT, GENERIC_ERROR, StackSize } from 'helpers/consts';
import { Alert, Button, Drawer, Dropdown, InlineSwitch, Input, Menu, Stack, useStyles2 } from '@grafana/ui';
import { DOCS_ROOT, StackSize, GENERIC_ERROR } from 'helpers/consts';
import { openErrorNotification } from 'helpers/helpers';
import { observer } from 'mobx-react';
@ -12,6 +12,10 @@ import { MonacoEditor, MonacoLanguage } from 'components/MonacoEditor/MonacoEdit
import { PluginLink } from 'components/PluginLink/PluginLink';
import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally';
import { Text } from 'components/Text/Text';
import {
getIsAddBtnDisabled,
getIsTooManyLabelsWarningVisible,
} from 'containers/IntegrationLabelsForm/IntegrationLabelsForm.helpers';
import { IntegrationTemplate } from 'containers/IntegrationTemplate/IntegrationTemplate';
import { splitToGroups } from 'models/label/label.helpers';
import { LabelsErrors } from 'models/label/label.types';
@ -19,12 +23,6 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
import { LabelTemplateOptions } from 'pages/integration/IntegrationCommon.config';
import { useStore } from 'state/useStore';
import { getIsAddBtnDisabled, getIsTooManyLabelsWarningVisible } from './IntegrationLabelsForm.helpers';
import styles from './IntegrationLabelsForm.module.css';
const cx = cn.bind(styles);
const INPUT_WIDTH = 280;
interface IntegrationLabelsFormProps {
@ -42,6 +40,7 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
const [showTemplateEditor, setShowTemplateEditor] = useState<boolean>(false);
const [customLabelsErrors, setCustomLabelsErrors] = useState<LabelsErrors>([]);
const [customLabelIndexToShowTemplateEditor, setCustomLabelIndexToShowTemplateEditor] = useState<number>(undefined);
const styles = useStyles2(getStyles);
const { alertReceiveChannelStore } = store;
@ -83,7 +82,12 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
scrollableContent
title="Alert group labeling"
subtitle={
<Text size="small" className="u-margin-top-xs">
<Text
size="small"
className={css`
margin-top: 4px;
`}
>
Combination of settings that manage the labeling of alert groups. More information in{' '}
<a href={`${DOCS_ROOT}/integrations/#alert-group-labels`} target="_blank" rel="noreferrer">
<Text type="link">documentation</Text>
@ -111,7 +115,7 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
Labels inherited from <PluginLink onClick={handleOpenIntegrationSettings}>the integration</PluginLink>
. This behavior can be disabled using the toggle option.
</Text>
<ul className={cx('labels-list')}>
<ul className={styles.labelsList}>
{alertReceiveChannel.labels.map((label) => (
<li key={label.key.id}>
<Stack gap={StackSize.xs}>
@ -147,10 +151,22 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
customLabelsErrors={customLabelsErrors}
/>
<Collapse isOpen={false} label="Multi-label extraction template" contentClassName="u-padding-top-none">
<Collapse
isOpen={false}
label="Multi-label extraction template"
contentClassName={css`
padding-top: none;
`}
>
<Stack direction="column">
<Stack justifyContent="space-between" alignItems="flex-end">
<Text type="secondary" size="small" className="u-padding-left-lg">
<Text
type="secondary"
size="small"
className={css`
padding-left: 24px;
`}
>
Allows for the extraction and modification of multiple labels from the alert payload using a single
template. Supports not only dynamic values but also dynamic keys. The Jinja template must result in
valid JSON dictionary.
@ -176,7 +192,7 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
</Stack>
</Collapse>
<div className={cx('buttons')}>
<div className={styles.buttons}>
<Stack justifyContent="flex-end">
<Button variant="secondary" onClick={onHide}>
Close
@ -188,6 +204,7 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
</div>
</Stack>
</Drawer>
{customLabelIndexToShowTemplateEditor !== undefined && (
<IntegrationTemplate
id={id}
@ -369,3 +386,22 @@ const CustomLabels = (props: CustomLabelsProps) => {
</Stack>
);
};
const getStyles = () => {
return {
labelsList: css`
margin: 0;
list-style-type: none;
> li {
margin: 10px 0;
}
`,
buttons: css`
width: 100%;
margin-top: 30px;
margin-bottom: 24px;
`,
};
};

View file

@ -1,75 +0,0 @@
.title-container {
padding: 24px 24px 0;
}
.container-wrapper {
padding: 8px;
height: 100%;
max-height: 100%;
}
.container {
display: flex;
height: 100%;
max-height: 100%;
width: 100%;
border: var(--border-strong);
}
.template-block-title {
padding: 16px;
align-items: baseline;
height: 56px;
}
.template-editor-block-title {
padding: 8px 16px 0;
align-items: baseline;
border: var(--border-weak);
background-color: var(--background-secondary);
height: 56px;
min-width: min-content;
}
.template-block-list,
.template-block-codeeditor {
overflow-y: hidden;
}
.template-block-list,
.template-block-codeeditor,
.template-block-result,
.result {
height: 100%;
max-height: 100%;
}
.template-block-list {
width: 30%;
}
.template-block-codeeditor {
width: 40%;
}
.template-block-result {
width: 30%;
overflow-y: scroll !important;
padding-right: 16px;
}
.result {
padding: 0;
padding-left: 16px;
padding-bottom: 60px;
}
.template-block-codeeditor div[aria-label='Code editor container'] {
border-bottom: none;
}
.template-editor-block-content {
height: calc(100% - 57px);
border-left: var(--border-weak);
border-right: var(--border-weak);
}

View file

@ -0,0 +1,83 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
const sharedMaxHeight = css`
height: 100%;
max-height: 100%;
`;
const overflowHidden = css`
overflow-y: hidden;
`;
export const getIntegrationTemplateStyles = (theme: GrafanaTheme2) => {
return {
titleContainer: css`
padding: 24px 24px 0;
`,
containerWrapper: css`
padding: 8px;
height: 100%;
max-height: 100%;
`,
container: css`
display: flex;
height: 100%;
max-height: 100%;
width: 100%;
border: 1px solid ${theme.colors.border.strong};
`,
templateBlockTitle: css`
padding: 16px;
align-items: baseline;
height: 56px;
`,
templateEditorBlockTitle: css`
padding: 8px 16px 0;
align-items: baseline;
border: 1px solid ${theme.colors.border.weak};
background-color: ${theme.colors.background.secondary}
height: 56px;
min-width: min-content;`,
templateBlockList: css`
width: 30%;
overflow-y: hidden;
${sharedMaxHeight}
${overflowHidden}
`,
templateBlockCodeEditor: css`
width: 40%;
overflow-y: hidden;
${sharedMaxHeight}
${overflowHidden}
div[aria-label='Code editor container'] {
border-bottom: none;
}
`,
templateBlockResult: css`
width: 30%;
overflow-y: scroll !important;
padding-right: 16px;
${sharedMaxHeight}
`,
result: css`
padding: 0;
padding-left: 16px;
padding-bottom: 60px;
${sharedMaxHeight}
`,
templateEditorBlockContent: css`
height: calc(100% - 57px);
border-left: 1px solid ${theme.colors.border.weak};
border-right: 1px solid ${theme.colors.border.weak};
`,
};
};

View file

@ -1,7 +1,6 @@
import React, { useCallback, useState, useEffect } from 'react';
import { Button, Drawer, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { Button, Drawer, Stack, useStyles2 } from '@grafana/ui';
import { LocationHelper } from 'helpers/LocationHelper';
import { UserActions } from 'helpers/authorization/authorization';
import { debounce } from 'lodash-es';
@ -29,9 +28,7 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
import { IntegrationTemplateOptions, LabelTemplateOptions } from 'pages/integration/IntegrationCommon.config';
import { useStore } from 'state/useStore';
import styles from './IntegrationTemplate.module.scss';
const cx = cn.bind(styles);
import { getIntegrationTemplateStyles } from './IntegrationTemplate.styles';
interface IntegrationTemplateProps {
id: ApiSchemas['AlertReceiveChannel']['id'];
@ -54,6 +51,7 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
const [resultError, setResultError] = useState<string>(undefined);
const [isRecentAlertGroupExisting, setIsRecentAlertGroupExisting] = useState<boolean>(false);
const styles = useStyles2(getIntegrationTemplateStyles);
const store = useStore();
useEffect(() => {
@ -164,7 +162,7 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
return (
<Drawer
title={
<div className={cx('title-container')}>
<div>
<Stack justifyContent="space-between" alignItems="flex-start">
<Stack direction="column">
<Text.Title level={3}>Edit {template.displayName} template</Text.Title>
@ -190,28 +188,26 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
closeOnMaskClick={false}
width={'95%'}
>
<div className={cx('container-wrapper')}>
<div className={cx('container')}>
<TemplatesAlertGroupsList
templatePage={TemplatePage.Integrations}
alertReceiveChannelId={id}
onEditPayload={onEditPayload}
onSelectAlertGroup={onSelectAlertGroup}
templates={templates}
onLoadAlertGroupsList={onLoadAlertGroupsList}
/>
{renderCheatSheet()}
<TemplateResult
alertReceiveChannelId={id}
template={template}
templateBody={changedTemplateBody}
isAlertGroupExisting={isRecentAlertGroupExisting}
chatOpsPermalink={chatOpsPermalink}
payload={alertGroupPayload}
error={resultError}
onSaveAndFollowLink={onSaveAndFollowLink}
/>
</div>
<div>
<TemplatesAlertGroupsList
templatePage={TemplatePage.Integrations}
alertReceiveChannelId={id}
onEditPayload={onEditPayload}
onSelectAlertGroup={onSelectAlertGroup}
templates={templates}
onLoadAlertGroupsList={onLoadAlertGroupsList}
/>
{renderCheatSheet()}
<TemplateResult
alertReceiveChannelId={id}
template={template}
templateBody={changedTemplateBody}
isAlertGroupExisting={isRecentAlertGroupExisting}
chatOpsPermalink={chatOpsPermalink}
payload={alertGroupPayload}
error={resultError}
onSaveAndFollowLink={onSaveAndFollowLink}
/>
</div>
</Drawer>
);
@ -229,8 +225,8 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
return (
<>
<div className={cx('template-block-codeeditor')}>
<div className={cx('template-editor-block-title')}>
<div className={styles.templateBlockCodeEditor}>
<div className={styles.templateEditorBlockTitle}>
<Stack justifyContent="space-between" alignItems="center" wrap="wrap">
<Text>Template editor</Text>
@ -239,7 +235,7 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
</Button>
</Stack>
</div>
<div className={cx('template-editor-block-content')}>
<div className={styles.templateEditorBlockContent}>
<MonacoEditor
value={changedTemplateBody}
data={templates}

View file

@ -1,5 +1,6 @@
import React, { forwardRef, useImperativeHandle, useState } from 'react';
import { css } from '@emotion/css';
import { ServiceLabelsProps, ServiceLabels } from '@grafana/labels';
import { Field, Label } from '@grafana/ui';
import { GENERIC_ERROR } from 'helpers/consts';
@ -88,7 +89,23 @@ const _Labels = observer(
return (
<div>
<Field label={<Label description={<div className="u-padding-vertical-xs">{description}</div>}>Labels</Label>}>
<Field
label={
<Label
description={
<div
className={css`
padding: 4px 0;
`}
>
{description}
</div>
}
>
Labels
</Label>
}
>
<ServiceLabels
loadById
value={value}

View file

@ -1,30 +0,0 @@
.info-block {
width: 752px;
text-align: center;
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
}
.field-command {
margin-top: 8px;
width: 752px;
}
.field-command input {
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: var(--primary-text-link);
}
.infoblock-text {
margin-left: 48px;
margin-right: 48px;
}
.done-button {
width: 752px;
direction: rtl;
}

View file

@ -1,9 +1,10 @@
import React, { FC } from 'react';
import { Button, Icon, Stack, Field, Input } from '@grafana/ui';
import cn from 'classnames/bind';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Icon, Stack, Field, Input, useStyles2 } from '@grafana/ui';
import { StackSize } from 'helpers/consts';
import { openNotification, openWarningNotification } from 'helpers/helpers';
import { openWarningNotification, openNotification } from 'helpers/helpers';
import { observer } from 'mobx-react';
import CopyToClipboard from 'react-copy-to-clipboard';
@ -13,8 +14,6 @@ import { Text } from 'components/Text/Text';
import MSTeamsLogo from 'icons/MSTeamsLogo';
import { useStore } from 'state/useStore';
import styles from './MSTeamsInstructions.module.css';
interface MSTeamsInstructionsProps {
onCallisAdded?: boolean;
showInfoBox?: boolean;
@ -23,9 +22,9 @@ interface MSTeamsInstructionsProps {
onHide?: () => void;
}
const cx = cn.bind(styles);
export const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props) => {
const styles = useStyles2(getStyles);
const { onCallisAdded, showInfoBox, personalSettings, onHide = () => {}, verificationCode } = props;
const { msteamsChannelStore } = useStore();
@ -44,7 +43,7 @@ export const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props
<Stack direction="column" alignItems="flex-start" gap={StackSize.lg}>
{!personalSettings && <Text.Title level={2}>Connect MS Teams workspace</Text.Title>}
{showInfoBox && (
<Block bordered withBackground className={cx('info-block')}>
<Block bordered withBackground className={styles.infoBlock}>
<Stack direction="column" alignItems="center">
<div style={{ width: '60px', marginTop: '24px' }}>
<MSTeamsLogo />
@ -55,7 +54,7 @@ export const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props
<Stack direction="column" alignItems="center">
<Text>This setup is for direct profile connection with bot. </Text>
<br />
<Text className={cx('infoblock-text')}>
<Text className={styles.infoblockText}>
To manage alert groups in Team channel, setup{' '}
<PluginLink query={{ page: 'chat-ops', tab: 'MSTeams' }}>Team ChatOps</PluginLink>
</Text>
@ -64,7 +63,7 @@ export const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props
<Stack direction="column" alignItems="center">
<Text>This setup is for Team channel connection with bot. </Text>
<br />
<Text className={cx('infoblock-text')}>
<Text className={styles.infoblockText}>
To manage alert groups in Direct Messages and verify users who are allowed to operate with MS Teams,
setup <PluginLink query={{ page: 'users', id: 'me' }}>personal MS Teams connection</PluginLink>
</Text>
@ -96,7 +95,7 @@ export const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props
command
</Text>
)}
<Field className={cx('field-command')}>
<Field className={styles.fieldCommand}>
<Input
id="msTeamsCommand"
value={verificationCode}
@ -113,7 +112,7 @@ export const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props
/>
</Field>
</Text>
<Block bordered withBackground className={cx('info-block')}>
<Block bordered withBackground className={styles.infoBlock}>
<Text type="secondary">
For more information please read{' '}
<a href="https://grafana.com/docs/oncall/latest/notify/ms-teams/" target="_blank" rel="noreferrer">
@ -123,10 +122,45 @@ export const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props
</Text>
</Block>
{!personalSettings && (
<div className={cx('done-button')}>
<div className={styles.doneButton}>
<Button onClick={handleMSTeamsGetChannels}>Done</Button>
</div>
)}
</Stack>
);
});
const getStyles = (theme: GrafanaTheme2) => {
return {
infoBlock: css`
width: 752px;
text-align: center;
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
`,
fieldCommand: css`
margin-top: 8px;
width: 752px;
input {
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: ${theme.colors.primary.text};
}
`,
infoblockText: css`
margin-left: 48px;
margin-right: 48px;
`,
doneButton: css`
width: 752px;
direction: rtl;
`,
};
};

View file

@ -1,25 +0,0 @@
.msteams-bot {
color: var(--primary-text-link);
}
.msteams-instruction-container {
display: block;
text-align: left;
margin-bottom: 12px;
}
.verification-code {
text-decoration: underline;
}
.copy-icon {
color: var(--primary-text-link);
}
.msteams-instruction-cancel {
margin-top: 24px;
}
.msTeams-modal {
min-width: 800px;
}

View file

@ -1,7 +1,7 @@
import React, { useCallback, useState, useEffect } from 'react';
import { Button, Modal } from '@grafana/ui';
import cn from 'classnames/bind';
import { css } from '@emotion/css';
import { Button, Modal, useStyles2 } from '@grafana/ui';
import { UserActions } from 'helpers/authorization/authorization';
import { observer } from 'mobx-react';
@ -9,10 +9,6 @@ import { MSTeamsInstructions } from 'containers/MSTeams/MSTeamsInstructions';
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
import { useStore } from 'state/useStore';
import styles from './MSTeamsIntegrationButton.module.css';
const cx = cn.bind(styles);
interface MSTeamsIntegrationProps {
disabled?: boolean;
size?: 'md' | 'lg';
@ -58,6 +54,7 @@ interface MSTeamsModalProps {
const MSTeamsModal = (props: MSTeamsModalProps) => {
const { onHide, onUpdate } = props;
const [verificationCode, setVerificationCode] = useState<string>();
const styles = useStyles2(getStyles);
const store = useStore();
useEffect(() => {
(async () => {
@ -67,8 +64,16 @@ const MSTeamsModal = (props: MSTeamsModalProps) => {
}, []);
return (
<Modal className={cx('msTeams-modal')} title="Connect MS Teams workspace" closeOnEscape isOpen onDismiss={onUpdate}>
<Modal className={styles.msteamsModal} title="Connect MS Teams workspace" closeOnEscape isOpen onDismiss={onUpdate}>
<MSTeamsInstructions onHide={onHide} verificationCode={verificationCode} onCallisAdded />
</Modal>
);
};
const getStyles = () => {
return {
msteamsModal: css`
min-width: 800px;
`,
};
};

View file

@ -1,11 +0,0 @@
.root {
display: block;
}
.title {
margin: 16px 0 0 16px;
}
.content {
margin: 4px 4px 400px 4px;
}

View file

@ -1,8 +1,8 @@
import React, { useCallback } from 'react';
import { css } from '@emotion/css';
import { SelectableValue } from '@grafana/data';
import { Button, Drawer, Field, Select, Stack, useStyles2 } from '@grafana/ui';
import cn from 'classnames/bind';
import { UserActions } from 'helpers/authorization/authorization';
import { openNotification, showApiError } from 'helpers/helpers';
import { observer } from 'mobx-react';
@ -17,10 +17,6 @@ import { MaintenanceMode } from 'models/alert_receive_channel/alert_receive_chan
import { ApiSchemas } from 'network/oncall-api/api.types';
import { useStore } from 'state/useStore';
import styles from './MaintenanceForm.module.css';
const cx = cn.bind(styles);
interface MaintenanceFormProps {
initialData: {
alert_receive_channel_id?: ApiSchemas['AlertReceiveChannel']['id'];
@ -69,11 +65,12 @@ export const MaintenanceForm = observer((props: MaintenanceFormProps) => {
formState: { errors },
} = formMethods;
const styles = useStyles2(getStyles);
const utils = useStyles2(getUtilStyles);
return (
<Drawer width="640px" scrollableContent title="Start Maintenance Mode" onClose={onHide} closeOnMaskClick={false}>
<div className={cx('content')} data-testid="maintenance-mode-drawer">
<div className={styles.content} data-testid="maintenance-mode-drawer">
<Stack direction="column">
Start maintenance mode when performing scheduled maintenance or updates on the infrastructure, which may
trigger false alarms.
@ -199,3 +196,11 @@ export const MaintenanceForm = observer((props: MaintenanceFormProps) => {
</Drawer>
);
});
const getStyles = () => {
return {
content: css`
margin: 4px 4px 400px 4px;
`,
};
};

View file

@ -1,88 +0,0 @@
.container {
display: flex;
flex-direction: row;
min-width: 100%;
&__box {
flex-basis: 50%;
}
&__box:first-child {
margin-right: 8px;
}
&__box:last-child {
margin-left: 8px;
}
}
@media (max-width: 768px) {
.container {
flex-direction: column;
&__box:first-child {
margin-right: 0px;
margin-bottom: 8px;
}
&__box:last-child {
margin-left: 0px;
margin-top: 8px;
}
}
}
.notification-buttons {
width: 100%;
padding-top: 12px;
}
.icon {
margin-top: -6px;
margin-left: 4px;
fill: var(--green-6);
}
.disconnect__container {
position: relative;
display: flex;
justify-content: center;
width: 100%;
}
.disconnect__qrCode {
width: 240px;
height: auto;
filter: blur(6px);
opacity: 0.6;
}
.blurry {
filter: blur(4px);
opacity: 0.2;
}
.qr-code {
background-color: #fff;
margin-bottom: 12px;
}
.qr-loader {
position: absolute;
z-index: 10;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
&__text {
text-align: center;
margin-bottom: 12px;
display: block;
}
i {
// Overwrite Grafana's loading icon
font-size: 32px;
}
}

View file

@ -0,0 +1,91 @@
import { css } from '@emotion/css';
import { Colors } from 'styles/utils.styles';
export const getMobileAppConnectionStyles = () => {
return {
container: css`
display: flex;
flex-direction: row;
min-width: 100%;
@media (max-width: 768px) {
flex-direction: column;
}
`,
containerBox: css`
flex-basis: 50%;
&:first-child {
margin-right: 8px;
}
*:last-child {
margin-left: 8px;
}
@media (max-width: 768px) {
&:first-child {
margin-right: 0px;
margin-bottom: 8px;
}
&:last-child {
margin-left: 0px;
margin-top: 8px;
}
}
`,
notificationButtons: css`
width: 100%;
padding-top: 12px;
`,
icon: css`
margin-top: -6px;
margin-left: 4px;
fill: ${Colors.GREEN_6};
`,
disconnectContainer: css`
position: relative;
display: flex;
justify-content: center;
width: 100%;
`,
disconnectQRCode: css`
width: 240px;
height: auto;
filter: blur(6px);
opacity: 0.6;
`,
blurry: css`
filter: blur(4px);
opacity: 0.2;
`,
qrCode: css`
background-color: #fff;
margin-bottom: 12px;
`,
qrLoader: css`
position: absolute;
z-index: 10;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
i {
font-size: 32px;
}
`,
qrLoaderText: css`
text-align: center;
margin-bottom: 12px;
display: block;
`,
};
};

View file

@ -1,10 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Icon, LoadingPlaceholder, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { css, cx } from '@emotion/css';
import { Button, Icon, LoadingPlaceholder, Stack, useStyles2 } from '@grafana/ui';
import { UserActions } from 'helpers/authorization/authorization';
import { StackSize } from 'helpers/consts';
import { isMobile, openErrorNotification, openNotification, openWarningNotification } from 'helpers/helpers';
import { isMobile, openNotification, openWarningNotification, openErrorNotification } from 'helpers/helpers';
import { useInitializePlugin } from 'helpers/hooks';
import { observer } from 'mobx-react';
@ -20,14 +20,12 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
import { AppFeature } from 'state/features';
import { RootStore, rootStore as store } from 'state/rootStore';
import styles from './MobileAppConnection.module.scss';
import { getMobileAppConnectionStyles } from './MobileAppConnection.styles';
import { DisconnectButton } from './parts/DisconnectButton/DisconnectButton';
import { DownloadIcons } from './parts/DownloadIcons/DownloadIcons';
import { LinkLoginButton } from './parts/LinkLoginButton/LinkLoginButton';
import { QRCode } from './parts/QRCode/QRCode';
const cx = cn.bind(styles);
type Props = {
userPk?: ApiSchemas['User']['pk'];
store?: RootStore;
@ -63,6 +61,8 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
const [isAttemptingTestNotification, setIsAttemptingTestNotification] = useState(false);
const isCurrentUser = userPk === undefined || userStore.currentUserPk === userPk;
const styles = useStyles2(getMobileAppConnectionStyles);
useEffect(() => {
isMounted.current = true;
@ -158,14 +158,14 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
content = (
<Stack direction="column" gap={StackSize.lg}>
<Text strong type="primary">
App connected <Icon name="check-circle" size="md" className={cx('icon')} />
App connected <Icon name="check-circle" size="md" className={styles.icon} />
</Text>
<Text type="primary">
You can only sync one application to your account. To setup a new device, please disconnect the currently
connected device first.
</Text>
<div className={cx('disconnect__container')}>
<img src={qrCodeImage} className={cx('disconnect__qrCode')} />
<div className={styles.disconnectContainer}>
<img src={qrCodeImage} className={styles.disconnectQRCode} />
<DisconnectButton onClick={disconnectMobileApp} />
</div>
</Stack>
@ -179,8 +179,16 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
<Text type="primary">
Open the Grafana OnCall mobile application and scan this code to sync it with your account.
</Text>
<div className={cx('u-width-100', 'u-flex', 'u-flex-center', 'u-position-relative')}>
<QRCode className={cx({ 'qr-code': true, blurry: isQRBlurry })} value={QRCodeValue} />
<div
className={css`
width: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
`}
>
<QRCode className={cx({ [styles.qrCode]: true, [styles.blurry]: isQRBlurry })} value={QRCodeValue} />
{isQRBlurry && <QRLoading />}
</div>
{store.isOpenSource && QRCodeDataParsed && (
@ -200,21 +208,21 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
<>
<h3>Mobile App Connection</h3>
<Stack direction="column">
<div className={cx('container')}>
<div className={styles.container}>
{QRCodeDataParsed && isMobile && (
<Block shadowed bordered withBackground className={cx('container__box')}>
<Block shadowed bordered withBackground className={styles.containerBox}>
<LinkLoginButton baseUrl={QRCodeDataParsed.oncall_api_url} token={QRCodeDataParsed.token} />
</Block>
)}
<Block shadowed bordered withBackground className={cx('container__box')}>
<Block shadowed bordered withBackground className={styles.containerBox}>
{content}
</Block>
<Block shadowed bordered withBackground className={cx('container__box')}>
<Block shadowed bordered withBackground className={styles.containerBox}>
<DownloadIcons />
</Block>
</div>
{mobileAppIsCurrentlyConnected && isCurrentUser && !disconnectingMobileApp && (
<div className={cx('notification-buttons')}>
<div className={styles.notificationButtons}>
<Stack gap={StackSize.md} justifyContent={'flex-end'}>
<Button
variant="secondary"
@ -356,9 +364,11 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
});
function QRLoading() {
const styles = useStyles2(getMobileAppConnectionStyles);
return (
<div className={cx('qr-loader')}>
<Text type="primary" className={cx('qr-loader__text')}>
<div className={styles.qrLoader}>
<Text type="primary" className={styles.qrLoaderText}>
Regenerating QR code...
</Text>
<LoadingPlaceholder text="Loading..." />

View file

@ -1,6 +0,0 @@
.disconnect-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View file

@ -1,28 +1,28 @@
import React, { FC } from 'react';
import { Button } from '@grafana/ui';
import cn from 'classnames/bind';
import { Button, useStyles2 } from '@grafana/ui';
import { getUtilStyles } from 'styles/utils.styles';
import { WithConfirm } from 'components/WithConfirm/WithConfirm';
import styles from './DisconnectButton.module.scss';
const cx = cn.bind(styles);
type Props = {
onClick: () => void;
};
export const DisconnectButton: FC<Props> = ({ onClick }) => (
<WithConfirm title="Are you sure to disconnect your mobile application?" confirmText="Remove">
<Button
variant="destructive"
onClick={onClick}
size="md"
className={cx('disconnect-button')}
data-testid="test__disconnect"
>
Disconnect
</Button>
</WithConfirm>
);
export const DisconnectButton: FC<Props> = ({ onClick }) => {
const utilStyles = useStyles2(getUtilStyles);
return (
<WithConfirm title="Are you sure to disconnect your mobile application?" confirmText="Remove">
<Button
variant="destructive"
onClick={onClick}
size="md"
className={utilStyles.centeredAbsolute}
data-testid="test__disconnect"
>
Disconnect
</Button>
</WithConfirm>
);
};

View file

@ -1,24 +0,0 @@
.icon {
width: 25px;
height: auto;
margin-right: 12px;
}
.icon-text,
.icon {
cursor: default;
}
.icon-block {
display: flex;
align-items: center;
min-height: 80px;
column-gap: 12px;
}
.icon-tag {
border-radius: 12px;
font-size: 12px;
padding: 2px 8px;
cursor: default;
}

View file

@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { css } from '@emotion/css';
import { Stack, useStyles2 } from '@grafana/ui';
import { StackSize } from 'helpers/consts';
import AppleLogoSVG from 'assets/img/apple-logo.svg';
@ -9,43 +9,72 @@ import PlayStoreLogoSVG from 'assets/img/play-store-logo.svg';
import { Block } from 'components/GBlock/Block';
import { Text } from 'components/Text/Text';
import styles from './DownloadIcons.module.scss';
export const DownloadIcons: FC = () => {
const styles = useStyles2(getStyles);
const cx = cn.bind(styles);
export const DownloadIcons: FC = () => (
<Stack direction="column" gap={StackSize.lg}>
<Text type="primary" strong>
Download
</Text>
<Text type="primary">The Grafana OnCall app is available on both the App Store and Google Play Store.</Text>
<Stack direction="column">
<a
style={{ width: '100%' }}
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
target="_blank"
rel="noreferrer"
>
<Block hover fullWidth withBackground bordered className={cx('icon-block')}>
<img src={AppleLogoSVG} alt="Apple" className={cx('icon')} />
<Text type="primary" className={cx('icon-text')}>
iOS
</Text>
</Block>
</a>
<a
style={{ width: '100%' }}
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
target="_blank"
rel="noreferrer"
>
<Block hover fullWidth bordered className={cx('icon-block')}>
<img src={PlayStoreLogoSVG} alt="Play Store" className={cx('icon')} />
<Text type="primary" className={cx('icon-text')}>
Android
</Text>
</Block>
</a>
return (
<Stack direction="column" gap={StackSize.lg}>
<Text type="primary" strong>
Download
</Text>
<Text type="primary">The Grafana OnCall app is available on both the App Store and Google Play Store.</Text>
<Stack direction="column">
<a
style={{ width: '100%' }}
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
target="_blank"
rel="noreferrer"
>
<Block hover fullWidth withBackground bordered className={styles.iconBlock}>
<img src={AppleLogoSVG} alt="Apple" className={styles.icon} />
<Text type="primary" className={styles.iconText}>
iOS
</Text>
</Block>
</a>
<a
style={{ width: '100%' }}
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
target="_blank"
rel="noreferrer"
>
<Block hover fullWidth bordered className={styles.iconBlock}>
<img src={PlayStoreLogoSVG} alt="Play Store" className={styles.icon} />
<Text type="primary" className={styles.iconText}>
Android
</Text>
</Block>
</a>
</Stack>
</Stack>
</Stack>
);
);
};
const getStyles = () => {
return {
icon: css`
width: 48px;
height: 48px;
cursor: default;
`,
iconBlock: css`
display: flex;
align-items: center;
min-height: 80px;
column-gap: 12px;
`,
iconText: css`
margin-left: 16px;
cursor: default;
`,
iconTag: css`
border-radius: 12px;
font-size: 12px;
padding: 2px 8px;
cursor: default;
`,
};
};

View file

@ -1,63 +0,0 @@
.root {
display: block;
}
.title {
margin: 0 0 0 16px;
}
.content {
margin: 4px;
}
.tabsWrapper {
padding-top: 16px;
}
.form-row {
display: flex;
flex-wrap: nowrap;
gap: 4px;
}
.form-field {
flex-grow: 1;
}
/* TODO: figure out why this is not picked */
.webhooks__drawerContent .cursor.monaco-mouse-cursor-text {
display: none !important;
}
.sourceCodeRoot {
height: calc(100vh - 530px);
min-height: 200px;
}
.cards {
display: flex;
flex-wrap: wrap;
gap: 24px;
overflow: auto;
scroll-snap-type: y mandatory;
width: 100%;
}
.card {
width: 100%;
height: 106px;
scroll-snap-align: start;
scroll-snap-stop: normal;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
cursor: pointer;
position: relative;
gap: 20px;
}
.search-integration {
width: 100%;
margin-bottom: 24px;
}

View file

@ -1,7 +1,7 @@
import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { Button, ConfirmModal, ConfirmModalProps, Drawer, Input, Tab, TabsBar, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { css, cx } from '@emotion/css';
import { Button, ConfirmModal, ConfirmModalProps, Drawer, Input, Tab, TabsBar, Stack, useStyles2 } from '@grafana/ui';
import { UserActions } from 'helpers/authorization/authorization';
import { PLUGIN_ROOT } from 'helpers/consts';
import { KeyValuePair } from 'helpers/helpers';
@ -23,10 +23,6 @@ import { TemplateParams, WebhookFormFieldName } from './OutgoingWebhookForm.type
import { OutgoingWebhookFormFields } from './OutgoingWebhookFormFields';
import { WebhookPresetBlocks } from './WebhookPresetBlocks';
import styles from './OutgoingWebhookForm.module.css';
const cx = cn.bind(styles);
interface OutgoingWebhookFormProps {
id: ApiSchemas['Webhook']['id'] | 'new';
action: WebhookFormActionType;
@ -192,6 +188,7 @@ const Presets = (props: PresetsProps) => {
const { onHide, onSelect } = props;
const [filterValue, setFilterValue] = useState('');
const styles = useStyles2(getWebhookFormStyles);
const { outgoingWebhookStore } = useStore();
@ -201,7 +198,7 @@ const Presets = (props: PresetsProps) => {
return (
<Drawer scrollableContent title="New Outgoing Webhook" onClose={onHide} closeOnMaskClick={false} width="640px">
<div className={cx('content')}>
<div className={styles.content}>
<Stack direction="column">
<Text type="secondary">
Outgoing webhooks can send alert data to other systems. They can be triggered by various conditions and can
@ -210,7 +207,7 @@ const Presets = (props: PresetsProps) => {
</Text>
{presets.length > 8 && (
<div className={cx('search-integration')}>
<div className={styles.searchIntegration}>
<Input
autoFocus
value={filterValue}
@ -241,20 +238,25 @@ const NewWebhook = (props: NewWebhookProps) => {
const { data, preset, onHide, action, onBack, onTemplateEditClick, onSubmit } = props;
const { hasFeature } = useStore();
const styles = useStyles2(getWebhookFormStyles);
const { handleSubmit } = useFormContext();
return (
<Drawer scrollableContent title={'New Outgoing Webhook'} onClose={onHide} closeOnMaskClick={false}>
<div className="webhooks__drawerContent">
<div className={cx('content')}>
<form id="OutgoingWebhook" onSubmit={handleSubmit(onSubmit)} className={styles.form}>
<div className={styles.webhooksDrawerContent}>
<div className={styles.content}>
<form id="OutgoingWebhook" onSubmit={handleSubmit(onSubmit)}>
<OutgoingWebhookFormFields
preset={preset}
hasLabelsFeature={hasFeature(AppFeature.Labels)}
onTemplateEditClick={onTemplateEditClick}
/>
<div className={cx('buttons')}>
<div
className={css`
padding-bottom: 24px;
`}
>
<Stack justifyContent="flex-end">
{action === WebhookFormActionType.NEW ? (
<Button variant="secondary" onClick={onBack}>
@ -295,6 +297,7 @@ const EditWebhookTabs = (props: EditWebhookTabsProps) => {
const { id, data, action, onHide, onUpdate, onDelete, onSubmit, onTemplateEditClick, preset } = props;
const navigate = useNavigate();
const styles = useStyles2(getWebhookFormStyles);
const [activeTab, setActiveTab] = useState(
action === WebhookFormActionType.EDIT_SETTINGS ? WebhookTabs.Settings.key : WebhookTabs.LastRun.key
@ -307,7 +310,11 @@ const EditWebhookTabs = (props: EditWebhookTabsProps) => {
onClose={onHide}
closeOnMaskClick={false}
tabs={
<div className={cx('tabsWrapper')}>
<div
className={css`
padding-top: 16px;
`}
>
<TabsBar>
<Tab
key={WebhookTabs.Settings.key}
@ -332,7 +339,7 @@ const EditWebhookTabs = (props: EditWebhookTabsProps) => {
</div>
}
>
<div className={cx('webhooks__drawerContent')}>
<div className={styles.webhooksDrawerContent}>
<WebhookTabsContent
id={id}
action={action}
@ -368,11 +375,12 @@ const WebhookTabsContent: React.FC<WebhookTabsProps> = observer(
const [confirmationModal, setConfirmationModal] = useState<ConfirmModalProps>(undefined);
const { hasFeature } = useStore();
const styles = useStyles2(getWebhookFormStyles);
const { handleSubmit } = useFormContext();
return (
<div className={cx('tabs__content')}>
<div>
{confirmationModal && (
<ConfirmModal
{...(confirmationModal as ConfirmModalProps)}
@ -382,14 +390,18 @@ const WebhookTabsContent: React.FC<WebhookTabsProps> = observer(
{activeTab === WebhookTabs.Settings.key && (
<>
<div className={cx('content')}>
<form id="OutgoingWebhook" onSubmit={handleSubmit(onSubmit)} className={styles.form}>
<div className={styles.content}>
<form id="OutgoingWebhook" onSubmit={handleSubmit(onSubmit)}>
<OutgoingWebhookFormFields
preset={preset}
hasLabelsFeature={hasFeature(AppFeature.Labels)}
onTemplateEditClick={onTemplateEditClick}
/>
<div className={cx('buttons')}>
<div
className={css`
padding-bottom: 24px;
`}
>
<Stack justifyContent={'flex-end'}>
<Button variant="secondary" onClick={onHide}>
Cancel
@ -436,3 +448,72 @@ const WebhookTabsContent: React.FC<WebhookTabsProps> = observer(
);
}
);
export const getWebhookFormStyles = () => {
return {
root: css`
display: block;
`,
title: css`
margin: 0 0 0 16px;
`,
content: css`
margin: 4px;
`,
tabsWrapper: css`
padding-top: 16px;
`,
formRow: css`
display: flex;
flex-wrap: nowrap;
gap: 4px;
`,
formField: css`
flex-grow: 1;
`,
webhooksDrawerContent: css`
.cursor.monaco-mouse-cursor-text {
display: none !important;
}
`,
sourceCodeRoot: css`
height: calc(100vh - 530px);
min-height: 200px;
`,
cards: css`
display: flex;
flex-wrap: wrap;
gap: 24px;
overflow: auto;
scroll-snap-type: y mandatory;
width: 100%;
`,
card: css`
width: 100%;
height: 106px;
scroll-snap-align: start;
scroll-snap-stop: normal;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
cursor: pointer;
position: relative;
gap: 20px;
`,
searchIntegration: css`
width: 100%;
margin-bottom: 24px;
`,
};
};

View file

@ -1,44 +1,42 @@
import React from 'react';
import { EmptySearchResult, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { EmptySearchResult, Stack, useStyles2 } from '@grafana/ui';
import { StackSize } from 'helpers/consts';
import { observer } from 'mobx-react';
import { Block } from 'components/GBlock/Block';
import { IntegrationLogo } from 'components/IntegrationLogo/IntegrationLogo';
import { logoCoors } from 'components/IntegrationLogo/IntegrationLogo.config';
import { logoColors } from 'components/IntegrationLogo/IntegrationLogo.config';
import { Text } from 'components/Text/Text';
import { getWebhookPresetIcons } from 'containers/OutgoingWebhookForm/WebhookPresetIcons.config';
import { OutgoingWebhookPreset } from 'models/outgoing_webhook/outgoing_webhook.types';
import { useStore } from 'state/useStore';
import styles from 'containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css';
const cx = cn.bind(styles);
import { getWebhookFormStyles } from './OutgoingWebhookForm';
export const WebhookPresetBlocks: React.FC<{
presets: OutgoingWebhookPreset[];
onBlockClick: (preset: OutgoingWebhookPreset) => void;
}> = observer(({ presets, onBlockClick }) => {
const store = useStore();
const styles = useStyles2(getWebhookFormStyles);
const webhookPresetIcons = getWebhookPresetIcons(store.features);
return (
<div className={cx('cards')} data-testid="create-outgoing-webhook-modal">
<div className={styles.cards} data-testid="create-outgoing-webhook-modal">
{presets.length ? (
presets.map((preset) => {
let logo = <IntegrationLogo integration={{ value: 'webhook', display_name: preset.name }} scale={0.2} />;
if (preset.logo in logoCoors) {
if (preset.logo in logoColors) {
logo = <IntegrationLogo integration={{ value: preset.logo, display_name: preset.name }} scale={0.2} />;
} else if (preset.logo in webhookPresetIcons) {
logo = webhookPresetIcons[preset.logo]();
}
return (
<Block bordered hover shadowed onClick={() => onBlockClick(preset)} key={preset.id} className={cx('card')}>
<div className={cx('card-bg')}>{logo}</div>
<div className={cx('title')}>
<Block bordered hover shadowed onClick={() => onBlockClick(preset)} key={preset.id} className={styles.card}>
<div>{logo}</div>
<div className={styles.title}>
<Stack direction="column" gap={StackSize.xs}>
<Stack>
<Text strong data-testid="webhook-preset-display-name">

View file

@ -1,18 +1,14 @@
import React from 'react';
import { Button, Stack } from '@grafana/ui';
import cn from 'classnames/bind';
import { Button, Stack, useStyles2 } from '@grafana/ui';
import { useCommonStyles } from 'helpers/hooks';
import { observer } from 'mobx-react';
import { WebhookLastEventDetails } from 'components/Webhooks/WebhookLastEventDetails';
import { getWebhookFormStyles } from 'containers/OutgoingWebhookForm/OutgoingWebhookForm';
import { ApiSchemas } from 'network/oncall-api/api.types';
import { useStore } from 'state/useStore';
import styles from 'containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css';
const cx = cn.bind(styles);
interface OutgoingWebhookStatusProps {
id: ApiSchemas['Webhook']['id'];
closeDrawer: () => void;
@ -25,10 +21,11 @@ export const OutgoingWebhookStatus = observer(({ id, closeDrawer }: OutgoingWebh
},
} = useStore();
const commonStyles = useCommonStyles();
const styles = useStyles2(getWebhookFormStyles);
return (
<div className={cx('content')}>
<WebhookLastEventDetails webhook={webhook} sourceCodeRootClassName={cx('sourceCodeRoot')} />
<div className={styles.content}>
<WebhookLastEventDetails webhook={webhook} sourceCodeRootClassName={styles.sourceCodeRoot} />
<div className={commonStyles.bottomDrawerButtons}>
<Stack justifyContent="flex-end">
<Button variant="secondary" onClick={closeDrawer}>

View file

@ -1,17 +0,0 @@
.root {
margin-bottom: 25px;
}
.root .steps {
margin: 15px 0 0 15px;
}
.sortable-helper {
z-index: 1062;
}
.root .step {
display: flex;
align-items: center;
margin-left: 10px;
}

View file

@ -1,12 +1,12 @@
import React, { useCallback } from 'react';
import { Button, Icon, LoadingPlaceholder, Stack, Tooltip } from '@grafana/ui';
import cn from 'classnames/bind';
import { css } from '@emotion/css';
import { Button, Icon, LoadingPlaceholder, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { UserActions } from 'helpers/authorization/authorization';
import { get } from 'lodash-es';
import { observer } from 'mobx-react';
import NotificationPolicy from 'components/Policy/NotificationPolicy';
import { NotificationPolicy } from 'components/Policy/NotificationPolicy';
import { SortableList } from 'components/SortableList/SortableList';
import { Text } from 'components/Text/Text';
import { Timeline } from 'components/Timeline/Timeline';
@ -19,10 +19,6 @@ import { useStore } from 'state/useStore';
import { getColor } from './PersonalNotificationSettings.helpers';
import img from './img/default-step.png';
import styles from './PersonalNotificationSettings.module.css';
const cx = cn.bind(styles);
interface PersonalNotificationSettingsProps {
userPk: ApiSchemas['User']['pk'];
isImportant: boolean;
@ -42,6 +38,8 @@ export const PersonalNotificationSettings = observer((props: PersonalNotificatio
[userPk, userStore]
);
const styles = useStyles2(getStyles);
const getAddNotificationPolicyHandler = useCallback(() => {
return () => {
userStore.addNotificationPolicy(userPk, isImportant);
@ -83,7 +81,7 @@ export const PersonalNotificationSettings = observer((props: PersonalNotificatio
if (!allNotificationPolicies) {
return (
<div className={cx('root')}>
<div className={styles.root}>
{title}
<LoadingPlaceholder text="Loading..." />
</div>
@ -116,12 +114,11 @@ export const PersonalNotificationSettings = observer((props: PersonalNotificatio
store.hasFeature(AppFeature.CloudConnection) && !store.cloudStore.cloudConnectionStatus.cloud_connection_status;
return (
<div className={cx('root')}>
<div className={styles.root}>
{title}
{/* @ts-ignore */}
<SortableList
helperClass={cx('sortable-helper')}
className={cx('steps')}
helperClass={styles.sortableHelper}
className={styles.steps}
axis="y"
lockAxis="y"
onSortEnd={getNotificationPolicySortEndHandler(offset)}
@ -154,7 +151,7 @@ export const PersonalNotificationSettings = observer((props: PersonalNotificatio
number={notificationPolicies.length + 1}
backgroundHexNumber={getColor(notificationPolicies.length)}
>
<div className={cx('step')}>
<div className={styles.step}>
<WithPermissionControlTooltip userAction={userAction}>
<Button icon="plus" variant="secondary" fill="text" onClick={getAddNotificationPolicyHandler()}>
Add Notification Step
@ -166,3 +163,25 @@ export const PersonalNotificationSettings = observer((props: PersonalNotificatio
</div>
);
});
const getStyles = () => {
return {
root: css`
margin-bottom: 25px;
`,
step: css`
display: flex;
align-items: center;
margin-left: 10px;
`,
steps: css`
margin: 15px 0 0 15px;
`,
sortableHelper: css`
z-index: 1062;
`,
};
};

View file

@ -42,7 +42,12 @@ export const PluginConfigPage = observer((props: PluginConfigPageProps<PluginMet
return (
<Stack direction="column">
<Text.Title level={3} className="u-margin-bottom-md">
<Text.Title
level={3}
className={css`
margin-bottom: 12px;
`}
>
Configure Grafana OnCall
</Text.Title>
{getIsRunningOpenSourceVersion() ? <OSSPluginConfigPage {...props} /> : <CloudPluginConfigPage {...props} />}
@ -267,7 +272,13 @@ const PluginConfigAlert = observer(() => {
shouldRender={showAlert}
render={() => (
<Alert severity="error" title="Plugin is not connected" onRemove={() => setShowAlert(false)}>
<ol className="u-margin-bottom-md">{errors}</ol>
<ol
className={css`
margin-bottom: 12px;
`}
>
{errors}
</ol>
<a href={PLUGIN_CONFIG} rel="noreferrer" onClick={() => window.location.reload()}>
<Text type="link">Reload</Text>
</a>

View file

@ -1,41 +0,0 @@
.root {
display: flex;
flex-direction: column;
width: 100%;
}
.filters {
display: flex;
gap: 10px;
padding: 10px;
border: var(--border);
border-radius: 2px;
flex-wrap: wrap;
}
.filter {
display: flex;
align-items: center;
gap: 0;
}
.root .filter-options {
width: 250px;
}
.root .filter-select {
min-width: 250px;
width: fit-content;
}
.infoIcon {
margin-left: 4px;
}
.border {
border: 1px solid red;
&:hover {
border: 1px solid red;
}
}

View file

@ -1,71 +0,0 @@
.root {
transition: background-color 300ms;
min-height: 28px;
overflow-x: hidden;
}
.loader {
height: 28px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.root:hover {
background: var(--secondary-background);
}
.timeline {
display: flex;
flex-direction: column;
gap: 5px;
padding-bottom: 4px;
position: relative;
}
.slots {
width: 100%;
display: flex;
transition: opacity 500ms ease;
opacity: 1;
}
.slots__transparent {
opacity: 0.5;
}
.current-time {
position: absolute;
left: 450px;
width: 1px;
background: var(--gradient-brandVertical);
top: -10px;
bottom: -10px;
z-index: 1;
}
.empty {
height: 28px;
cursor: pointer;
text-align: center;
/* background: #5f505633;
border: 1px dashed #5c474d;
color: rgba(209, 14, 92, 0.5); */
margin: 0 2px;
}
.pointer {
position: absolute;
top: -9px;
transition: left 500ms ease, opacity 500ms ease, transform 500ms ease;
transform-origin: bottom center;
opacity: 0;
transform: scale(0);
}
.pointer--active {
transform: scale(1);
opacity: 1;
}

View file

@ -0,0 +1,74 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
export const getRotationStyles = (theme: GrafanaTheme2) => {
return {
root: css`
transition: background-color 300ms;
min-height: 28px;
overflow-x: hidden;
`,
loader: css`
height: 28px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
&:hover {
background: ${theme.colors.background.secondary};
}
`,
timeline: css`
display: flex;
flex-direction: column;
gap: 5px;
padding-bottom: 4px;
position: relative;
`,
slots: css`
width: 100%;
display: flex;
transition: opacity 500ms ease;
opacity: 1;
`,
slotsTransparent: css`
opacity: 0.5;
`,
currentTime: css`
position: absolute;
left: 450px;
width: 1px;
background: ${theme.colors.gradients.brandVertical};
top: -10px;
bottom: -10px;
z-index: 1;
`,
empty: css`
height: 28px;
cursor: pointer;
text-align: center;
margin: 0 2px;
`,
pointer: css`
position: absolute;
top: -9px;
transition: left 500ms ease, opacity 500ms ease, transform 500ms ease;
transform-origin: bottom center;
opacity: 0;
transform: scale(0);
&--active {
opacity: 1;
transform: scale(1);
}
`,
};
};

Some files were not shown because too many files have changed in this diff Show more