Migrate remaining stylesheets to Emotion (#5022)
Closes https://github.com/grafana/oncall/issues/4201
This commit is contained in:
parent
c7a7a3f81a
commit
d8d4a58035
197 changed files with 3588 additions and 3812 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
`,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
.root {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
39
grafana-plugin/src/components/Timeline/Timeline.styles.ts
Normal file
39
grafana-plugin/src/components/Timeline/Timeline.styles.ts
Normal 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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -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={
|
||||
(
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.select {
|
||||
width: 150px !important;
|
||||
}
|
||||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
.root {
|
||||
border-radius: 2px;
|
||||
background: var(--secondary-background);
|
||||
padding: 2px 2px 2px 12px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -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};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -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`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
.form {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.incident-matcher {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.root {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
.root {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--success-text-color);
|
||||
}
|
||||
|
|
@ -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};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.root {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.root {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
.root {
|
||||
min-width: 200px;
|
||||
|
||||
& > div {
|
||||
// If not set then inner div will not benefit of min-width
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
.root {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.teamSelectLabel {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.teamSelectLink {
|
||||
color: var(--primary-text-link);
|
||||
}
|
||||
|
||||
.teamSelectInfo {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
.instruction {
|
||||
ol,
|
||||
ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
.root {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 16px 0 0 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 4px 4px 400px 4px;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
@ -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..." />
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
.disconnect-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
74
grafana-plugin/src/containers/Rotation/Rotation.styles.ts
Normal file
74
grafana-plugin/src/containers/Rotation/Rotation.styles.ts
Normal 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
Loading…
Add table
Reference in a new issue