Fixed deprecated imports of H/VGroup in favor of Stack (#4897)
# What this PR does Closes https://github.com/grafana/irm/issues/10
This commit is contained in:
parent
3269c9b3a7
commit
0965c6ab75
170 changed files with 1220 additions and 4110 deletions
4
.github/workflows/expensive-e2e-tests.yml
vendored
4
.github/workflows/expensive-e2e-tests.yml
vendored
|
|
@ -14,8 +14,8 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
grafana_version:
|
grafana_version:
|
||||||
- 10.1.7
|
- 10.3.0
|
||||||
- 10.3.3
|
- latest
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
# Run one version at a time to avoid the issue when SMS notification are bundled together for multiple versions
|
# Run one version at a time to avoid the issue when SMS notification are bundled together for multiple versions
|
||||||
# running at the same time (the affected test is in grafana-plugin/e2e-tests/alerts/sms.test.ts)
|
# running at the same time (the affected test is in grafana-plugin/e2e-tests/alerts/sms.test.ts)
|
||||||
|
|
|
||||||
7
.github/workflows/linting-and-tests.yml
vendored
7
.github/workflows/linting-and-tests.yml
vendored
|
|
@ -242,11 +242,8 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
grafana_version:
|
grafana_version:
|
||||||
- 10.1.7
|
- 10.3.0
|
||||||
- 10.3.3
|
- latest
|
||||||
# TODO: fix issues with running e2e tests against Grafana v10.2.x and latest
|
|
||||||
# - 10.2.4
|
|
||||||
# - latest
|
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
with:
|
with:
|
||||||
grafana_version: ${{ matrix.grafana_version }}
|
grafana_version: ${{ matrix.grafana_version }}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ module.exports = {
|
||||||
{
|
{
|
||||||
files: ['src/**/*.{ts,tsx}'],
|
files: ['src/**/*.{ts,tsx}'],
|
||||||
rules: {
|
rules: {
|
||||||
'deprecation/deprecation': 'off',
|
'deprecation/deprecation': 'warn',
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: './tsconfig.json',
|
project: './tsconfig.json',
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ test.describe('maintenance mode works', () => {
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
const integrationSettingsPopupElement = page
|
const integrationSettingsPopupElement = page
|
||||||
.getByTestId('integration-settings-context-menu-wrapper')
|
.getByTestId('integration-settings-context-menu-wrapper')
|
||||||
.getByRole('img');
|
.locator('svg');
|
||||||
|
|
||||||
await integrationSettingsPopupElement.click();
|
await integrationSettingsPopupElement.click();
|
||||||
/**
|
/**
|
||||||
* sometimes we need to click twice (e.g. adding the escalation chain route
|
* sometimes we need to click twice (e.g. adding the escalation chain route
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,14 @@ test('create advanced webhook and check it is displayed on the list correctly',
|
||||||
// Enter webhook name
|
// Enter webhook name
|
||||||
await webhooksFormDivs.locator('[name=name]').fill(WEBHOOK_NAME);
|
await webhooksFormDivs.locator('[name=name]').fill(WEBHOOK_NAME);
|
||||||
|
|
||||||
// Select team
|
// Open team dropdown
|
||||||
await page.getByLabel('New Outgoing Webhook').getByRole('img').nth(1).click(); // Open team dropdown
|
await page.getByTestId('team-selector').locator('div').filter({ hasText: 'Choose (Optional)' }).nth(1).click();
|
||||||
await page.getByLabel('Select options menu').getByText('No team').click(); // Select "No team"
|
// Set No Team
|
||||||
|
await page.getByLabel('Select options menu').getByText('No team').click();
|
||||||
|
|
||||||
// Select trigger type
|
// Select trigger type
|
||||||
await webhooksFormDivs.filter({ hasText: 'Trigger Type' }).getByRole('img').click();
|
await page.getByTestId('triggerType-selector').locator('div').nth(1).click();
|
||||||
|
|
||||||
await page.getByLabel('Select options menu').getByText('Resolved', { exact: true }).click();
|
await page.getByLabel('Select options menu').getByText('Resolved', { exact: true }).click();
|
||||||
|
|
||||||
// Select integration
|
// Select integration
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,12 @@ const createWebhook = async ({ page, webhookName, webhookUrl }) => {
|
||||||
|
|
||||||
await page.keyboard.insertText(webhookUrl);
|
await page.keyboard.insertText(webhookUrl);
|
||||||
await page.locator('[name=name]').fill(webhookName);
|
await page.locator('[name=name]').fill(webhookName);
|
||||||
await page.getByLabel('New Outgoing Webhook').getByRole('img').nth(1).click(); // Open team dropdown
|
|
||||||
|
// Open team dropdown
|
||||||
|
await page.getByTestId('team-selector').locator('div').filter({ hasText: 'Choose (Optional)' }).nth(1).click();
|
||||||
|
// Set No Team
|
||||||
await page.getByLabel('Select options menu').getByText('No team').click();
|
await page.getByLabel('Select options menu').getByText('No team').click();
|
||||||
|
|
||||||
await clickButton({ page, buttonText: 'Create' });
|
await clickButton({ page, buttonText: 'Create' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ test.describe('Plugin configuration', () => {
|
||||||
adminRolePage: { page },
|
adminRolePage: { page },
|
||||||
}) => {
|
}) => {
|
||||||
await goToGrafanaPage(page, PLUGIN_CONFIG);
|
await goToGrafanaPage(page, PLUGIN_CONFIG);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
const correctURLAppliedByDefault = await page.getByTestId('oncall-api-url-input').inputValue();
|
const correctURLAppliedByDefault = await page.getByTestId('oncall-api-url-input').inputValue();
|
||||||
|
|
||||||
// show client-side validation errors
|
// show client-side validation errors
|
||||||
|
|
@ -27,6 +28,7 @@ test.describe('Plugin configuration', () => {
|
||||||
|
|
||||||
// apply back correct url and verify plugin connected again
|
// apply back correct url and verify plugin connected again
|
||||||
await urlInput.fill(correctURLAppliedByDefault);
|
await urlInput.fill(correctURLAppliedByDefault);
|
||||||
|
await page.waitForTimeout(500);
|
||||||
await page.getByTestId('connect-plugin').click();
|
await page.getByTestId('connect-plugin').click();
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
await page.getByText('Plugin is connected').waitFor();
|
await page.getByText('Plugin is connected').waitFor();
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,13 @@ test('Fills in override time and reacts to timezone change', async ({ adminRoleP
|
||||||
await expect(overrideEndEl.getByTestId('date-time-picker').getByRole('textbox')).toHaveValue('09:00');
|
await expect(overrideEndEl.getByTestId('date-time-picker').getByRole('textbox')).toHaveValue('09:00');
|
||||||
|
|
||||||
async function changeDatePickerTime(element: Locator, value: string) {
|
async function changeDatePickerTime(element: Locator, value: string) {
|
||||||
await element.getByRole('img').click();
|
await element.getByTestId('date-time-picker').getByRole('textbox').click();
|
||||||
|
|
||||||
// set minutes to {value}
|
// set minutes to {value}
|
||||||
await page.locator('.rc-time-picker-panel').getByRole('button', { name: value }).first().click();
|
await page.getByRole('button', { name: value }).first().click();
|
||||||
|
|
||||||
|
// Old way
|
||||||
|
// await page.locator('.rc-time-picker-panel').getByRole('button', { name: value }).first().click();
|
||||||
// set seconds to 00
|
// set seconds to 00
|
||||||
await page.getByRole('button', { name: '00' }).nth(1).click();
|
await page.getByRole('button', { name: '00' }).nth(1).click();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,15 @@ test('Fills in Rotation time and reacts to timezone change', async ({ adminRole
|
||||||
await expect(endEl.getByTestId('date-time-picker').getByRole('textbox')).toHaveValue('09:00');
|
await expect(endEl.getByTestId('date-time-picker').getByRole('textbox')).toHaveValue('09:00');
|
||||||
|
|
||||||
async function changeDatePickerTime(element: Locator, value: string) {
|
async function changeDatePickerTime(element: Locator, value: string) {
|
||||||
await element.getByRole('img').click();
|
await element.getByTestId('date-time-picker').getByRole('textbox').click();
|
||||||
|
|
||||||
// set minutes to {value}
|
// set minutes to {value}
|
||||||
await page.locator('.rc-time-picker-panel').getByRole('button', { name: value }).first().click();
|
await page.getByRole('button', { name: value }).first().click();
|
||||||
|
// await page.getByRole('button', { name: seconds }).nth(1).click();
|
||||||
|
|
||||||
|
// Old way
|
||||||
|
// await page.locator('.rc-time-picker-panel').getByRole('button', { name: value }).first().click();
|
||||||
|
|
||||||
// set seconds to 00
|
// set seconds to 00
|
||||||
await page.getByRole('button', { name: '00' }).nth(1).click();
|
await page.getByRole('button', { name: '00' }).nth(1).click();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ test('dates in schedule are correct according to selected current timezone', asy
|
||||||
await expect(page.getByTestId('timezone-select')).toHaveText('GMT+3');
|
await expect(page.getByTestId('timezone-select')).toHaveText('GMT+3');
|
||||||
|
|
||||||
// Change timezone to GMT
|
// Change timezone to GMT
|
||||||
await page.getByTestId('timezone-select').getByRole('img').click();
|
await page.getByTestId('timezone-select').locator('div').filter({ hasText: 'GMT+' }).nth(1).click();
|
||||||
await page.getByText('GMT', { exact: true }).click();
|
await page.getByText('GMT', { exact: true }).click();
|
||||||
|
|
||||||
// Selected timezone and local time is correctly displayed
|
// Selected timezone and local time is correctly displayed
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { VerticalGroup, useStyles2 } from '@grafana/ui';
|
import { Stack, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { Block } from 'components/GBlock/Block';
|
import { Block } from 'components/GBlock/Block';
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import { getCardButtonStyles } from './CardButton.styles';
|
import { getCardButtonStyles } from './CardButton.styles';
|
||||||
|
|
||||||
|
|
@ -30,10 +31,10 @@ export const CardButton: FC<CardButtonProps> = (props) => {
|
||||||
>
|
>
|
||||||
<div className={styles.icon}>{icon}</div>
|
<div className={styles.icon}>{icon}</div>
|
||||||
<div className={styles.meta}>
|
<div className={styles.meta}>
|
||||||
<VerticalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
<Text type="secondary">{description}</Text>
|
<Text type="secondary">{description}</Text>
|
||||||
<Text.Title level={1}>{title}</Text.Title>
|
<Text.Title level={1}>{title}</Text.Title>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { HorizontalGroup, IconButton, VerticalGroup, useStyles2 } from '@grafana/ui';
|
import { IconButton, Stack, useStyles2 } from '@grafana/ui';
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
import { bem, getUtilStyles } from 'styles/utils.styles';
|
import { bem, getUtilStyles } from 'styles/utils.styles';
|
||||||
|
|
||||||
import { Block } from 'components/GBlock/Block';
|
import { Block } from 'components/GBlock/Block';
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
import { openNotification } from 'utils/utils';
|
import { openNotification } from 'utils/utils';
|
||||||
|
|
||||||
import { CheatSheetInterface, CheatSheetItem } from './CheatSheet.config';
|
import { CheatSheetInterface, CheatSheetItem } from './CheatSheet.config';
|
||||||
|
|
@ -26,11 +27,11 @@ export const CheatSheet = (props: CheatSheetProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.cheatsheetContainer}>
|
<div className={styles.cheatsheetContainer}>
|
||||||
<div className={styles.cheatsheetInnerContainer}>
|
<div className={styles.cheatsheetInnerContainer}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<Text strong>{cheatSheetName} cheatsheet</Text>
|
<Text strong>{cheatSheetName} cheatsheet</Text>
|
||||||
<IconButton aria-label="Close" name="times" onClick={onClose} />
|
<IconButton aria-label="Close" name="times" onClick={onClose} />
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<Text type="secondary">{cheatSheetData.description}</Text>
|
<Text type="secondary">{cheatSheetData.description}</Text>
|
||||||
<div className={utils.width100}>
|
<div className={utils.width100}>
|
||||||
{cheatSheetData.fields?.map((field: CheatSheetItem) => {
|
{cheatSheetData.fields?.map((field: CheatSheetItem) => {
|
||||||
|
|
@ -41,7 +42,7 @@ export const CheatSheet = (props: CheatSheetProps) => {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -60,7 +61,7 @@ const CheatSheetListItem = (props: CheatSheetListItemProps) => {
|
||||||
{field.listItems?.map((item, key) => {
|
{field.listItems?.map((item, key) => {
|
||||||
return (
|
return (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<VerticalGroup spacing="md">
|
<Stack direction="column" gap={StackSize.md}>
|
||||||
{item.listItemName && (
|
{item.listItemName && (
|
||||||
<li style={{ margin: '0 0 0 4px' }}>
|
<li style={{ margin: '0 0 0 4px' }}>
|
||||||
<Text>{item.listItemName}</Text>
|
<Text>{item.listItemName}</Text>
|
||||||
|
|
@ -69,18 +70,18 @@ const CheatSheetListItem = (props: CheatSheetListItemProps) => {
|
||||||
{item.codeExample && (
|
{item.codeExample && (
|
||||||
<div className={bem(styles.cheatsheetItem, 'small')}>
|
<div className={bem(styles.cheatsheetItem, 'small')}>
|
||||||
<Block bordered fullWidth withBackground>
|
<Block bordered fullWidth withBackground>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<Text type="link" className={styles.code}>
|
<Text type="link" className={styles.code}>
|
||||||
{item.codeExample}
|
{item.codeExample}
|
||||||
</Text>
|
</Text>
|
||||||
<CopyToClipboard text={item.codeExample} onCopy={() => openNotification('Example copied')}>
|
<CopyToClipboard text={item.codeExample} onCopy={() => openNotification('Example copied')}>
|
||||||
<IconButton aria-label="Copy" name="copy" />
|
<IconButton aria-label="Copy" name="copy" />
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Block>
|
</Block>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { Button, HorizontalGroup, Icon, Select } from '@grafana/ui';
|
import { Button, Icon, Select, Stack } from '@grafana/ui';
|
||||||
|
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
interface CursorPaginationProps {
|
interface CursorPaginationProps {
|
||||||
current: string;
|
current: string;
|
||||||
|
|
@ -30,8 +31,8 @@ export const CursorPagination: FC<CursorPaginationProps> = (props) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup spacing="md" justify="flex-end">
|
<Stack gap={StackSize.md} justifyContent="flex-end">
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Text type="secondary">Items per list</Text>
|
<Text type="secondary">Items per list</Text>
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
|
@ -39,8 +40,8 @@ export const CursorPagination: FC<CursorPaginationProps> = (props) => {
|
||||||
value={itemsPerPage}
|
value={itemsPerPage}
|
||||||
onChange={onChangeItemsPerPageCallback}
|
onChange={onChangeItemsPerPageCallback}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Button
|
<Button
|
||||||
aria-label="previous"
|
aria-label="previous"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -66,7 +67,7 @@ export const CursorPagination: FC<CursorPaginationProps> = (props) => {
|
||||||
>
|
>
|
||||||
<Icon name="angle-right" />
|
<Icon name="angle-right" />
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useStyles2, VerticalGroup } from '@grafana/ui';
|
import { useStyles2, Stack } from '@grafana/ui';
|
||||||
|
|
||||||
import errorSVG from 'assets/img/error.svg';
|
import errorSVG from 'assets/img/error.svg';
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
interface FullPageErrorProps {
|
interface FullPageErrorProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|
@ -21,12 +22,12 @@ export const FullPageError: FC<FullPageErrorProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<VerticalGroup align="center" spacing="md">
|
<Stack direction="column" alignItems="center" gap={StackSize.md}>
|
||||||
<img src={errorSVG} alt="" />
|
<img src={errorSVG} alt="" />
|
||||||
<Text.Title level={3}>{title}</Text.Title>
|
<Text.Title level={3}>{title}</Text.Title>
|
||||||
{subtitle && <Text type="secondary">{subtitle}</Text>}
|
{subtitle && <Text type="secondary">{subtitle}</Text>}
|
||||||
{children}
|
{children}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,7 @@
|
||||||
import React, { useEffect, useReducer } from 'react';
|
import React, { useEffect, useReducer } from 'react';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import {
|
import { Button, Drawer, Icon, IconButton, Input, RadioButtonGroup, Select, Tooltip, Stack } from '@grafana/ui';
|
||||||
Button,
|
|
||||||
Drawer,
|
|
||||||
HorizontalGroup,
|
|
||||||
Icon,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
RadioButtonGroup,
|
|
||||||
Select,
|
|
||||||
Tooltip,
|
|
||||||
VerticalGroup,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -26,7 +15,7 @@ import { ContactPoint } from 'models/alert_receive_channel/alert_receive_channel
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import styles from 'pages/integration/Integration.module.scss';
|
import styles from 'pages/integration/Integration.module.scss';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
import { GENERIC_ERROR } from 'utils/consts';
|
import { GENERIC_ERROR, StackSize } from 'utils/consts';
|
||||||
import { openErrorNotification, openNotification } from 'utils/utils';
|
import { openErrorNotification, openNotification } from 'utils/utils';
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
const cx = cn.bind(styles);
|
||||||
|
|
@ -122,33 +111,33 @@ export const IntegrationContactPoint: React.FC<{
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={cx('contactpoints__connect')}>
|
<div className={cx('contactpoints__connect')}>
|
||||||
<VerticalGroup spacing="md">
|
<Stack direction="column" gap={StackSize.md}>
|
||||||
<div
|
<div
|
||||||
className={cx('contactpoints__connect-toggler')}
|
className={cx('contactpoints__connect-toggler')}
|
||||||
onClick={() => setState({ isConnectOpen: !isConnectOpen })}
|
onClick={() => setState({ isConnectOpen: !isConnectOpen })}
|
||||||
>
|
>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<HorizontalGroup spacing="xs" align="center">
|
<Stack gap={StackSize.xs} alignItems="center">
|
||||||
<Text type="primary">Grafana Alerting Contact point</Text>
|
<Text type="primary">Grafana Alerting Contact point</Text>
|
||||||
<Icon name="info-circle" />
|
<Icon name="info-circle" />
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
|
|
||||||
{isConnectOpen ? <Icon name="arrow-down" /> : <Icon name="arrow-right" />}
|
{isConnectOpen ? <Icon name="arrow-down" /> : <Icon name="arrow-right" />}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{renderConnectSection()}
|
{renderConnectSection()}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<HorizontalGroup spacing="md">
|
<Stack gap={StackSize.md}>
|
||||||
<IntegrationTag>Contact point</IntegrationTag>
|
<IntegrationTag>Contact point</IntegrationTag>
|
||||||
|
|
||||||
{contactPoints?.length ? (
|
{contactPoints?.length ? (
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Text type="primary">
|
<Text type="primary">
|
||||||
{contactPoints.length} contact point{contactPoints.length === 1 ? '' : 's'} connected
|
{contactPoints.length} contact point{contactPoints.length === 1 ? '' : 's'} connected
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -163,16 +152,16 @@ export const IntegrationContactPoint: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<HorizontalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
{renderExclamationIcon()}
|
{renderExclamationIcon()}
|
||||||
<Text type="primary" data-testid="integration-escalation-chain-not-selected">
|
<Text type="primary" data-testid="integration-escalation-chain-not-selected">
|
||||||
Connect Alerting Contact point to receive alerts
|
Connect Alerting Contact point to receive alerts
|
||||||
</Text>
|
</Text>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant={'secondary'}
|
variant={'secondary'}
|
||||||
|
|
@ -194,7 +183,7 @@ export const IntegrationContactPoint: React.FC<{
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalGroup spacing="md">
|
<Stack direction="column" gap={StackSize.md}>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
options={radioOptions}
|
options={radioOptions}
|
||||||
value={isExistingContactPoint ? 'existing' : 'new'}
|
value={isExistingContactPoint ? 'existing' : 'new'}
|
||||||
|
|
@ -233,7 +222,7 @@ export const IntegrationContactPoint: React.FC<{
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<HorizontalGroup align="center">
|
<Stack alignItems="center">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={!selectedAlertManager || !selectedContactPoint || isLoading}
|
disabled={!selectedAlertManager || !selectedContactPoint || isLoading}
|
||||||
|
|
@ -245,8 +234,8 @@ export const IntegrationContactPoint: React.FC<{
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{isLoading && <Icon name="fa fa-spinner" size="md" className={cx('loadingPlaceholder')} />}
|
{isLoading && <Icon name="fa fa-spinner" size="md" className={cx('loadingPlaceholder')} />}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -263,7 +252,7 @@ export const IntegrationContactPoint: React.FC<{
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup spacing="md">
|
<Stack gap={StackSize.md}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Alert Manager"
|
aria-label="Alert Manager"
|
||||||
name="external-link-alt"
|
name="external-link-alt"
|
||||||
|
|
@ -278,23 +267,23 @@ export const IntegrationContactPoint: React.FC<{
|
||||||
title={`Disconnect Contact point`}
|
title={`Disconnect Contact point`}
|
||||||
confirmText="Disconnect"
|
confirmText="Disconnect"
|
||||||
description={
|
description={
|
||||||
<VerticalGroup spacing="md">
|
<Stack direction="column" gap={StackSize.md}>
|
||||||
<Text type="primary">
|
<Text type="primary">
|
||||||
When the contact point will be disconnected, the Integration will no longer receive alerts for it.
|
When the contact point will be disconnected, the Integration will no longer receive alerts for it.
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="primary">You can add new contact point at any time.</Text>
|
<Text type="primary">You can add new contact point at any time.</Text>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconButton aria-label="Disconnect Contact Point" name="trash-alt" onClick={onDisconnect} />
|
<IconButton aria-label="Disconnect Contact Point" name="trash-alt" onClick={onDisconnect} />
|
||||||
</WithConfirm>
|
</WithConfirm>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderContactPointName(item: ContactPoint) {
|
function renderContactPointName(item: ContactPoint) {
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
<Text type="primary">{item.contactPoint}</Text>
|
<Text type="primary">{item.contactPoint}</Text>
|
||||||
|
|
||||||
{!item.notificationConnected && (
|
{!item.notificationConnected && (
|
||||||
|
|
@ -305,7 +294,7 @@ export const IntegrationContactPoint: React.FC<{
|
||||||
{renderExclamationIcon()}
|
{renderExclamationIcon()}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
|
import { Icon, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { noop } from 'lodash-es';
|
import { noop } from 'lodash-es';
|
||||||
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { Text } from 'components/Text/Text';
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import styles from 'pages/integration/Integration.module.scss';
|
import styles from 'pages/integration/Integration.module.scss';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
const cx = cn.bind(styles);
|
||||||
|
|
||||||
|
|
@ -50,10 +51,10 @@ export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveCha
|
||||||
className={cx('u-pull-right')}
|
className={cx('u-pull-right')}
|
||||||
>
|
>
|
||||||
<Text type="link" size="small">
|
<Text type="link" size="small">
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
How it works
|
How it works
|
||||||
<Icon name="external-link-alt" />
|
<Icon name="external-link-alt" />
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Text>
|
</Text>
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
|
|
@ -74,10 +75,10 @@ export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveCha
|
||||||
className={cx('u-pull-right')}
|
className={cx('u-pull-right')}
|
||||||
>
|
>
|
||||||
<Text type="link" size="small">
|
<Text type="link" size="small">
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
How to connect
|
How to connect
|
||||||
<Icon name="external-link-alt" />
|
<Icon name="external-link-alt" />
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Text>
|
</Text>
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
|
|
@ -98,14 +99,14 @@ export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveCha
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalGroup justify={'flex-start'} spacing={'xs'}>
|
<Stack direction="column" justifyContent={'flex-start'} gap={StackSize.xs}>
|
||||||
{!hasAlerts && (
|
{!hasAlerts && (
|
||||||
<HorizontalGroup spacing={'xs'}>
|
<Stack gap={StackSize.xs}>
|
||||||
<Icon name="fa fa-spinner" size="md" className={cx('loadingPlaceholder')} />
|
<Icon name="fa fa-spinner" size="md" className={cx('loadingPlaceholder')} />
|
||||||
<Text type={'primary'}>No alerts yet</Text> {callToAction()}
|
<Text type={'primary'}>No alerts yet</Text> {callToAction()}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { HorizontalGroup, IconButton, Input, useStyles2 } from '@grafana/ui';
|
import { IconButton, Input, Stack, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { CopyToClipboardIcon } from 'components/CopyToClipboardIcon/CopyToClipboardIcon';
|
import { CopyToClipboardIcon } from 'components/CopyToClipboardIcon/CopyToClipboardIcon';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import { getIntegrationInputFieldStyles } from './IntegrationInputField.styles';
|
import { getIntegrationInputFieldStyles } from './IntegrationInputField.styles';
|
||||||
|
|
||||||
|
|
@ -38,11 +39,11 @@ export const IntegrationInputField: React.FC<IntegrationInputFieldProps> = ({
|
||||||
<div className={styles.inputContainer}>{renderInputField()}</div>
|
<div className={styles.inputContainer}>{renderInputField()}</div>
|
||||||
|
|
||||||
<div className={cx(styles.icons, iconsClassName)}>
|
<div className={cx(styles.icons, iconsClassName)}>
|
||||||
<HorizontalGroup spacing={'xs'}>
|
<Stack gap={StackSize.xs}>
|
||||||
{showEye && <IconButton aria-label="Reveal" name={'eye'} size={'xs'} onClick={onInputReveal} />}
|
{showEye && <IconButton aria-label="Reveal" name={'eye'} size={'xs'} onClick={onInputReveal} />}
|
||||||
{showCopy && <CopyToClipboardIcon text={value} iconButtonProps={{ size: 'xs' }} />}
|
{showCopy && <CopyToClipboardIcon text={value} iconButtonProps={{ size: 'xs' }} />}
|
||||||
{showExternal && <IconButton aria-label="Open" name={'external-link-alt'} size={'xs'} onClick={onOpen} />}
|
{showExternal && <IconButton aria-label="Open" name={'external-link-alt'} size={'xs'} onClick={onOpen} />}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { HorizontalGroup } from '@grafana/ui';
|
import { Stack } from '@grafana/ui';
|
||||||
|
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import { IntegrationLogo, IntegrationLogoProps } from './IntegrationLogo';
|
import { IntegrationLogo, IntegrationLogoProps } from './IntegrationLogo';
|
||||||
|
|
||||||
|
|
@ -11,8 +12,8 @@ interface IntegrationLogoWithTitleProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IntegrationLogoWithTitle: FC<IntegrationLogoWithTitleProps> = ({ integration }) => (
|
export const IntegrationLogoWithTitle: FC<IntegrationLogoWithTitleProps> = ({ integration }) => (
|
||||||
<HorizontalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
<IntegrationLogo scale={0.08} integration={integration} />
|
<IntegrationLogo scale={0.08} integration={integration} />
|
||||||
<Text type="primary">{integration?.display_name}</Text>
|
<Text type="primary">{integration?.display_name}</Text>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { Button, HorizontalGroup, Icon, Modal, Tooltip, VerticalGroup } from '@grafana/ui';
|
import { Button, Icon, Modal, Tooltip, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
import Emoji from 'react-emoji-render';
|
import Emoji from 'react-emoji-render';
|
||||||
|
|
@ -14,6 +14,7 @@ import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_re
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import styles from 'pages/integration/Integration.module.scss';
|
import styles from 'pages/integration/Integration.module.scss';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
import { openNotification } from 'utils/utils';
|
import { openNotification } from 'utils/utils';
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
const cx = cn.bind(styles);
|
||||||
|
|
@ -42,18 +43,18 @@ export const IntegrationSendDemoAlertModal: React.FC<IntegrationSendDemoPayloadM
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onDismiss={onHideOrCancel}
|
onDismiss={onHideOrCancel}
|
||||||
title={
|
title={
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Text.Title level={4}>
|
<Text.Title level={4}>
|
||||||
Send demo alert to integration: {''}
|
Send demo alert to integration: {''}
|
||||||
<strong>
|
<strong>
|
||||||
<Emoji text={alertReceiveChannel.verbal_name} />
|
<Emoji text={alertReceiveChannel.verbal_name} />
|
||||||
</strong>
|
</strong>
|
||||||
</Text.Title>
|
</Text.Title>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<HorizontalGroup spacing={'xs'}>
|
<Stack gap={StackSize.xs}>
|
||||||
<Text type={'secondary'}>Alert Payload</Text>
|
<Text type={'secondary'}>Alert Payload</Text>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
|
|
@ -66,7 +67,7 @@ export const IntegrationSendDemoAlertModal: React.FC<IntegrationSendDemoPayloadM
|
||||||
>
|
>
|
||||||
<Icon name={'info-circle'} />
|
<Icon name={'info-circle'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
|
|
||||||
<div className={cx('integration__payloadInput')}>
|
<div className={cx('integration__payloadInput')}>
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
|
|
@ -82,7 +83,7 @@ export const IntegrationSendDemoAlertModal: React.FC<IntegrationSendDemoPayloadM
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HorizontalGroup justify={'flex-end'} spacing={'md'}>
|
<Stack justifyContent={'flex-end'} gap={StackSize.md}>
|
||||||
<Button variant={'secondary'} onClick={onHideOrCancel}>
|
<Button variant={'secondary'} onClick={onHideOrCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -92,8 +93,8 @@ export const IntegrationSendDemoAlertModal: React.FC<IntegrationSendDemoPayloadM
|
||||||
<Button variant={'primary'} onClick={onSendAlert} data-testid="submit-send-alert">
|
<Button variant={'primary'} onClick={onSendAlert} data-testid="submit-send-alert">
|
||||||
Send Alert
|
Send Alert
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { LabelTag } from '@grafana/labels';
|
import { LabelTag } from '@grafana/labels';
|
||||||
import { VerticalGroup, HorizontalGroup, Button, Tooltip } from '@grafana/ui';
|
import { Stack, Button, Tooltip } from '@grafana/ui';
|
||||||
|
|
||||||
import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally';
|
import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally';
|
||||||
import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge';
|
import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge';
|
||||||
import { LabelKeyValue } from 'models/label/label.types';
|
import { LabelKeyValue } from 'models/label/label.types';
|
||||||
import { components } from 'network/oncall-api/autogenerated-api.types';
|
import { components } from 'network/oncall-api/autogenerated-api.types';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
interface LabelsTooltipBadgeProps {
|
interface LabelsTooltipBadgeProps {
|
||||||
labels: LabelKeyValue[];
|
labels: LabelKeyValue[];
|
||||||
|
|
@ -21,9 +22,9 @@ export const LabelsTooltipBadge: FC<LabelsTooltipBadgeProps> = ({ labels, onClic
|
||||||
addPadding
|
addPadding
|
||||||
text={labels?.length}
|
text={labels?.length}
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<VerticalGroup spacing="sm">
|
<Stack direction="column" gap={StackSize.sm}>
|
||||||
{labels.map((label) => (
|
{labels.map((label) => (
|
||||||
<HorizontalGroup spacing="sm" key={label.key.id}>
|
<Stack gap={StackSize.sm} key={label.key.id}>
|
||||||
<LabelTag label={label.key.name} value={label.value.name} />
|
<LabelTag label={label.key.name} value={label.value.name} />
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -32,9 +33,9 @@ export const LabelsTooltipBadge: FC<LabelsTooltipBadgeProps> = ({ labels, onClic
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => onClick(label)}
|
onClick={() => onClick(label)}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
@ -47,16 +48,16 @@ interface LabelBadgesProps {
|
||||||
export const LabelBadges: React.FC<LabelBadgesProps> = ({ labels = [], maxCount = 3 }) => {
|
export const LabelBadges: React.FC<LabelBadgesProps> = ({ labels = [], maxCount = 3 }) => {
|
||||||
const renderer = (values: LabelBadgesProps['labels']) => {
|
const renderer = (values: LabelBadgesProps['labels']) => {
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
{values.map((label) => (
|
{values.map((label) => (
|
||||||
<LabelTag key={label.key.id} label={label.key.name} value={label.value.name} />
|
<LabelTag key={label.key.id} label={label.key.name} value={label.value.name} />
|
||||||
))}
|
))}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup spacing="sm">
|
<Stack gap={StackSize.sm}>
|
||||||
{renderer(labels.slice(0, maxCount))}
|
{renderer(labels.slice(0, maxCount))}
|
||||||
|
|
||||||
<RenderConditionally shouldRender={labels.length > maxCount}>
|
<RenderConditionally shouldRender={labels.length > maxCount}>
|
||||||
|
|
@ -64,6 +65,6 @@ export const LabelBadges: React.FC<LabelBadgesProps> = ({ labels = [], maxCount
|
||||||
<div>{labels.length > maxCount ? `+ ${labels.length - maxCount}` : ``}</div>
|
<div>{labels.length > maxCount ? `+ ${labels.length - maxCount}` : ``}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</RenderConditionally>
|
</RenderConditionally>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC, useCallback } from 'react';
|
import React, { FC, useCallback } from 'react';
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Button, Drawer, Field, HorizontalGroup, TextArea, useStyles2, VerticalGroup } from '@grafana/ui';
|
import { Button, Drawer, Field, TextArea, useStyles2, Stack } from '@grafana/ui';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||||
import { getUtilStyles } from 'styles/utils.styles';
|
import { getUtilStyles } from 'styles/utils.styles';
|
||||||
|
|
@ -70,7 +70,7 @@ export const ManualAlertGroup: FC<ManualAlertGroupProps> = observer(({ onCreate,
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer scrollableContent title="New escalation" onClose={onHideDrawer} closeOnMaskClick={false} width="70%">
|
<Drawer scrollableContent title="New escalation" onClose={onHideDrawer} closeOnMaskClick={false} width="70%">
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
<form id="Manual Alert Group" onSubmit={handleSubmit(onSubmit)} className={utilStyles.width100}>
|
<form id="Manual Alert Group" onSubmit={handleSubmit(onSubmit)} className={utilStyles.width100}>
|
||||||
<Controller
|
<Controller
|
||||||
|
|
@ -87,18 +87,18 @@ export const ManualAlertGroup: FC<ManualAlertGroupProps> = observer(({ onCreate,
|
||||||
<AddResponders mode="create" />
|
<AddResponders mode="create" />
|
||||||
|
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
<HorizontalGroup justify="flex-end">
|
<Stack justifyContent="flex-end">
|
||||||
<Button variant="secondary" onClick={onHideDrawer}>
|
<Button variant="secondary" onClick={onHideDrawer}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={!formIsSubmittable}>
|
<Button type="submit" disabled={!formIsSubmittable}>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC, useCallback, useState } from 'react';
|
import React, { FC, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Button, Drawer, HorizontalGroup, Icon, VerticalGroup, useStyles2 } from '@grafana/ui';
|
import { Button, Drawer, Icon, Stack, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { Block } from 'components/GBlock/Block';
|
import { Block } from 'components/GBlock/Block';
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
|
|
@ -9,6 +9,7 @@ import { ScheduleForm } from 'containers/ScheduleForm/ScheduleForm';
|
||||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||||
import { Schedule, ScheduleType } from 'models/schedule/schedule.types';
|
import { Schedule, ScheduleType } from 'models/schedule/schedule.types';
|
||||||
import { UserActions } from 'utils/authorization/authorization';
|
import { UserActions } from 'utils/authorization/authorization';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
interface NewScheduleSelectorProps {
|
interface NewScheduleSelectorProps {
|
||||||
onHide: () => void;
|
onHide: () => void;
|
||||||
|
|
@ -34,52 +35,52 @@ export const NewScheduleSelector: FC<NewScheduleSelectorProps> = ({ onHide, onCr
|
||||||
return (
|
return (
|
||||||
<Drawer scrollableContent title="Create new schedule" onClose={onHide} closeOnMaskClick={false}>
|
<Drawer scrollableContent title="Create new schedule" onClose={onHide} closeOnMaskClick={false}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<VerticalGroup spacing="lg">
|
<Stack direction="column" gap={StackSize.lg}>
|
||||||
<Block bordered withBackground className={styles.block}>
|
<Block bordered withBackground className={styles.block}>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<HorizontalGroup spacing="md">
|
<Stack gap={StackSize.md}>
|
||||||
<Icon name="calendar-alt" size="xl" />
|
<Icon name="calendar-alt" size="xl" />
|
||||||
<VerticalGroup spacing="none">
|
<Stack direction="column" gap={StackSize.none}>
|
||||||
<Text type="primary" size="large">
|
<Text type="primary" size="large">
|
||||||
Set up on-call rotation schedule
|
Set up on-call rotation schedule
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="secondary">Configure rotations and shifts directly in Grafana On-Call</Text>
|
<Text type="secondary">Configure rotations and shifts directly in Grafana On-Call</Text>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<WithPermissionControlTooltip userAction={UserActions.SchedulesWrite}>
|
<WithPermissionControlTooltip userAction={UserActions.SchedulesWrite}>
|
||||||
<Button variant="primary" icon="plus" onClick={getCreateScheduleClickHandler(ScheduleType.API)}>
|
<Button variant="primary" icon="plus" onClick={getCreateScheduleClickHandler(ScheduleType.API)}>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Block>
|
</Block>
|
||||||
<Block bordered withBackground className={styles.block}>
|
<Block bordered withBackground className={styles.block}>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<HorizontalGroup spacing="md">
|
<Stack gap={StackSize.md}>
|
||||||
<Icon name="download-alt" size="xl" />
|
<Icon name="download-alt" size="xl" />
|
||||||
<VerticalGroup spacing="none">
|
<Stack direction="column" gap={StackSize.none}>
|
||||||
<Text type="primary" size="large">
|
<Text type="primary" size="large">
|
||||||
Import schedule from iCal Url
|
Import schedule from iCal Url
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="secondary">Import rotations and shifts from your calendar app</Text>
|
<Text type="secondary">Import rotations and shifts from your calendar app</Text>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<Button variant="secondary" icon="plus" onClick={getCreateScheduleClickHandler(ScheduleType.Ical)}>
|
<Button variant="secondary" icon="plus" onClick={getCreateScheduleClickHandler(ScheduleType.Ical)}>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Block>
|
</Block>
|
||||||
<Block bordered withBackground className={styles.block}>
|
<Block bordered withBackground className={styles.block}>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<HorizontalGroup spacing="md">
|
<Stack gap={StackSize.md}>
|
||||||
<Icon name="cog" size="xl" />
|
<Icon name="cog" size="xl" />
|
||||||
<VerticalGroup spacing="none">
|
<Stack direction="column" gap={StackSize.none}>
|
||||||
<Text type="primary" size="large">
|
<Text type="primary" size="large">
|
||||||
Create schedule by API
|
Create schedule by API
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="secondary">Use API or Terraform to manage rotations</Text>
|
<Text type="secondary">Use API or Terraform to manage rotations</Text>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://grafana.com/blog/2022/08/29/get-started-with-grafana-oncall-and-terraform/"
|
href="https://grafana.com/blog/2022/08/29/get-started-with-grafana-oncall-and-terraform/"
|
||||||
|
|
@ -87,9 +88,9 @@ export const NewScheduleSelector: FC<NewScheduleSelectorProps> = ({ onHide, onCr
|
||||||
>
|
>
|
||||||
<Button variant="secondary">Read more</Button>
|
<Button variant="secondary">Read more</Button>
|
||||||
</a>
|
</a>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Block>
|
</Block>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import React, { ComponentProps, FC } from 'react';
|
import React, { ComponentProps, FC } from 'react';
|
||||||
|
|
||||||
import { HorizontalGroup, Icon, Tooltip } from '@grafana/ui';
|
import { Icon, Stack, Tooltip } from '@grafana/ui';
|
||||||
|
|
||||||
interface NonExistentUserNameProps {
|
interface NonExistentUserNameProps {
|
||||||
justify?: ComponentProps<typeof HorizontalGroup>['justify'];
|
justify?: ComponentProps<typeof Stack>['justifyContent'];
|
||||||
userName?: string;
|
userName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NonExistentUserName: FC<NonExistentUserNameProps> = ({ justify = 'space-between', userName }) => (
|
const NonExistentUserName: FC<NonExistentUserNameProps> = ({ justify = 'space-between', userName }) => (
|
||||||
<HorizontalGroup justify={justify}>
|
<Stack justifyContent={justify}>
|
||||||
<span>Missing user</span>
|
<span>Missing user</span>
|
||||||
<Tooltip content={`${userName || 'User'} } is not found or doesn't have permission to participate in the rotation`}>
|
<Tooltip content={`${userName || 'User'} } is not found or doesn't have permission to participate in the rotation`}>
|
||||||
<Icon name="exclamation-triangle" />
|
<Icon name="exclamation-triangle" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default NonExistentUserName;
|
export default NonExistentUserName;
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { VerticalGroup, useStyles2 } from '@grafana/ui';
|
import { Stack, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { PluginLink } from 'components/PluginLink/PluginLink';
|
import { PluginLink } from 'components/PluginLink/PluginLink';
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
import { openWarningNotification } from 'utils/utils';
|
import { openWarningNotification } from 'utils/utils';
|
||||||
|
|
||||||
export interface PageBaseState {
|
export interface PageBaseState {
|
||||||
|
|
@ -53,7 +54,7 @@ export const PageErrorHandlingWrapper = function ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.notFound}>
|
<div className={styles.notFound}>
|
||||||
<VerticalGroup spacing="lg" align="center">
|
<Stack direction="column" gap={StackSize.lg} alignItems="center">
|
||||||
<Text.Title level={1} className={styles.errorCode}>
|
<Text.Title level={1} className={styles.errorCode}>
|
||||||
403
|
403
|
||||||
</Text.Title>
|
</Text.Title>
|
||||||
|
|
@ -66,7 +67,7 @@ export const PageErrorHandlingWrapper = function ({
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
Or return to the <PluginLink query={{ page: pageName }}>{objectName} list</PluginLink>
|
Or return to the <PluginLink query={{ page: pageName }}>{objectName} list</PluginLink>
|
||||||
</Text>
|
</Text>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { ChangeEvent } from 'react';
|
import React, { ChangeEvent } from 'react';
|
||||||
|
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { Button, Input, Select, IconButton, withTheme2, Themeable2 } from '@grafana/ui';
|
import { Button, Input, Select, IconButton, withTheme2 } from '@grafana/ui';
|
||||||
import { isNumber } from 'lodash-es';
|
import { isNumber } from 'lodash-es';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
|
|
@ -57,7 +57,9 @@ interface EscalationPolicyBaseProps {
|
||||||
|
|
||||||
// We export the base props class, the actual definition is wrapped by MobX
|
// We export the base props class, the actual definition is wrapped by MobX
|
||||||
// MobX adds extra props that we do not need to pass on the consuming side
|
// MobX adds extra props that we do not need to pass on the consuming side
|
||||||
export interface EscalationPolicyProps extends EscalationPolicyBaseProps, ElementSortableProps, Themeable2 {}
|
export interface EscalationPolicyProps extends EscalationPolicyBaseProps, ElementSortableProps {
|
||||||
|
theme: GrafanaTheme2;
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
|
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { Button, IconButton, Select, Themeable2, withTheme2 } from '@grafana/ui';
|
import { Button, IconButton, Select, withTheme2 } from '@grafana/ui';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
import { SortableElement } from 'react-sortable-hoc';
|
import { SortableElement } from 'react-sortable-hoc';
|
||||||
|
|
||||||
|
|
@ -22,7 +22,8 @@ import { DragHandle } from './DragHandle';
|
||||||
import { POLICY_DURATION_LIST_MINUTES, POLICY_DURATION_LIST_SECONDS } from './Policy.consts';
|
import { POLICY_DURATION_LIST_MINUTES, POLICY_DURATION_LIST_SECONDS } from './Policy.consts';
|
||||||
import { PolicyNote } from './PolicyNote';
|
import { PolicyNote } from './PolicyNote';
|
||||||
|
|
||||||
export interface NotificationPolicyProps extends Themeable2 {
|
export interface NotificationPolicyProps {
|
||||||
|
theme: GrafanaTheme2;
|
||||||
data: NotificationPolicyType;
|
data: NotificationPolicyType;
|
||||||
slackTeamIdentity?: {
|
slackTeamIdentity?: {
|
||||||
general_log_channel_pk: Channel['id'];
|
general_log_channel_pk: Channel['id'];
|
||||||
|
|
@ -46,28 +47,18 @@ export interface NotificationPolicyProps extends Themeable2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotificationPolicy extends React.Component<NotificationPolicyProps, any> {
|
export class NotificationPolicy extends React.Component<NotificationPolicyProps, any> {
|
||||||
private styles: ReturnType<typeof getStyles>;
|
|
||||||
|
|
||||||
constructor(props: NotificationPolicyProps) {
|
constructor(props: NotificationPolicyProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.styles = getStyles(this.props.theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Readonly<NotificationPolicyProps>): void {
|
|
||||||
if (prevProps.theme !== this.props.theme) {
|
|
||||||
// fetch new styles whenever the theme changes
|
|
||||||
this.styles = getStyles(this.props.theme);
|
|
||||||
this.forceUpdate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, notificationChoices, number, color, userAction, isDisabled } = this.props;
|
const { data, notificationChoices, number, color, userAction, isDisabled, theme } = this.props;
|
||||||
const { id, step } = data;
|
const { id, step } = data;
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline.Item className={cx(this.styles.root)} number={number} backgroundHexNumber={color}>
|
<Timeline.Item className={cx(styles.root)} number={number} backgroundHexNumber={color}>
|
||||||
<div className={cx(this.styles.step)}>
|
<div className={cx(styles.step)}>
|
||||||
{!isDisabled && (
|
{!isDisabled && (
|
||||||
<WithPermissionControlTooltip userAction={userAction}>
|
<WithPermissionControlTooltip userAction={userAction}>
|
||||||
<DragHandle />
|
<DragHandle />
|
||||||
|
|
@ -75,7 +66,7 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
|
||||||
)}
|
)}
|
||||||
<WithPermissionControlTooltip userAction={userAction}>
|
<WithPermissionControlTooltip userAction={userAction}>
|
||||||
<Select
|
<Select
|
||||||
className={cx(this.styles.select, this.styles.control)}
|
className={cx(styles.select, styles.control)}
|
||||||
onChange={this._getOnChangeHandler('step')}
|
onChange={this._getOnChangeHandler('step')}
|
||||||
value={step}
|
value={step}
|
||||||
options={notificationChoices.map((option: any) => ({ label: option.display_name, value: option.value }))}
|
options={notificationChoices.map((option: any) => ({ label: option.display_name, value: option.value }))}
|
||||||
|
|
@ -86,7 +77,7 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
|
||||||
<WithPermissionControlTooltip userAction={userAction}>
|
<WithPermissionControlTooltip userAction={userAction}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Remove"
|
aria-label="Remove"
|
||||||
className={cx(this.styles.control)}
|
className={cx(styles.control)}
|
||||||
name="trash-alt"
|
name="trash-alt"
|
||||||
onClick={this._getDeleteClickHandler(id)}
|
onClick={this._getDeleteClickHandler(id)}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|
@ -185,9 +176,11 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderWaitDelays(disabled: boolean) {
|
private _renderWaitDelays(disabled: boolean) {
|
||||||
const { data, userAction } = this.props;
|
const { data, userAction, theme } = this.props;
|
||||||
const { wait_delay } = data;
|
const { wait_delay } = data;
|
||||||
|
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
const optionsList = [...POLICY_DURATION_LIST_MINUTES];
|
const optionsList = [...POLICY_DURATION_LIST_MINUTES];
|
||||||
|
|
||||||
const waitDelayInSeconds = parseFloat(wait_delay);
|
const waitDelayInSeconds = parseFloat(wait_delay);
|
||||||
|
|
@ -200,10 +193,10 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WithPermissionControlTooltip userAction={userAction}>
|
<WithPermissionControlTooltip userAction={userAction}>
|
||||||
<div className={this.styles.container}>
|
<div className={styles.container}>
|
||||||
<Select
|
<Select
|
||||||
key="wait-delay"
|
key="wait-delay"
|
||||||
className={cx(this.styles.delay, this.styles.control)}
|
className={cx(styles.delay, styles.control)}
|
||||||
value={wait_delay ? optionValue : undefined}
|
value={wait_delay ? optionValue : undefined}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(option: SelectableValue) => this._getOnChangeHandler('wait_delay')({ value: option.value * 60 })}
|
onChange={(option: SelectableValue) => this._getOnChangeHandler('wait_delay')({ value: option.value * 60 })}
|
||||||
|
|
@ -234,15 +227,17 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderNotifyBy(disabled: boolean) {
|
private _renderNotifyBy(disabled: boolean) {
|
||||||
const { data, notifyByOptions = [], userAction } = this.props;
|
const { data, notifyByOptions = [], theme, userAction } = this.props;
|
||||||
const { notify_by } = data;
|
const { notify_by } = data;
|
||||||
|
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WithPermissionControlTooltip userAction={userAction}>
|
<WithPermissionControlTooltip userAction={userAction}>
|
||||||
<Select
|
<Select
|
||||||
key="notify_by"
|
key="notify_by"
|
||||||
placeholder="Notify by"
|
placeholder="Notify by"
|
||||||
className={cx(this.styles.select, this.styles.control)}
|
className={cx(styles.select, styles.control)}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
value={notify_by}
|
value={notify_by}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC, useEffect } from 'react';
|
import React, { FC, useEffect } from 'react';
|
||||||
|
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { Tooltip, VerticalGroup, useStyles2 } from '@grafana/ui';
|
import { Tooltip, Stack, useStyles2 } from '@grafana/ui';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { getUtilStyles } from 'styles/utils.styles';
|
import { getUtilStyles } from 'styles/utils.styles';
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { Text } from 'components/Text/Text';
|
||||||
import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge';
|
import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge';
|
||||||
import { Schedule, ScheduleScoreQualityResult } from 'models/schedule/schedule.types';
|
import { Schedule, ScheduleScoreQualityResult } from 'models/schedule/schedule.types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import { getScheduleQualityStyles } from './ScheduleQuality.styles';
|
import { getScheduleQualityStyles } from './ScheduleQuality.styles';
|
||||||
|
|
||||||
|
|
@ -50,7 +51,7 @@ export const ScheduleQuality: FC<ScheduleQualityProps> = observer(({ schedule })
|
||||||
text={schedule.number_of_escalation_chains}
|
text={schedule.number_of_escalation_chains}
|
||||||
tooltipTitle="Used in escalations"
|
tooltipTitle="Used in escalations"
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<VerticalGroup spacing="sm">
|
<Stack direction="column" gap={StackSize.sm}>
|
||||||
{relatedScheduleEscalationChains.map((escalationChain) => (
|
{relatedScheduleEscalationChains.map((escalationChain) => (
|
||||||
<div key={escalationChain.pk}>
|
<div key={escalationChain.pk}>
|
||||||
<PluginLink query={{ page: 'escalations', id: escalationChain.pk }} className="link">
|
<PluginLink query={{ page: 'escalations', id: escalationChain.pk }} className="link">
|
||||||
|
|
@ -58,7 +59,7 @@ export const ScheduleQuality: FC<ScheduleQualityProps> = observer(({ schedule })
|
||||||
</PluginLink>
|
</PluginLink>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -71,13 +72,13 @@ export const ScheduleQuality: FC<ScheduleQualityProps> = observer(({ schedule })
|
||||||
text={schedule.warnings.length}
|
text={schedule.warnings.length}
|
||||||
tooltipTitle="Warnings"
|
tooltipTitle="Warnings"
|
||||||
tooltipContent={
|
tooltipContent={
|
||||||
<VerticalGroup spacing="none">
|
<Stack direction="column" gap={StackSize.none}>
|
||||||
{schedule.warnings.map((warning, index) => (
|
{schedule.warnings.map((warning, index) => (
|
||||||
<Text type="primary" key={index}>
|
<Text type="primary" key={index}>
|
||||||
{warning}
|
{warning}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import React, { FC, useCallback, useState } from 'react';
|
import React, { FC, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { HorizontalGroup, Icon, IconButton, useStyles2 } from '@grafana/ui';
|
import { Icon, IconButton, Stack, useStyles2 } from '@grafana/ui';
|
||||||
import { bem, getUtilStyles } from 'styles/utils.styles';
|
import { bem, getUtilStyles } from 'styles/utils.styles';
|
||||||
|
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
import { ScheduleScoreQualityResponse, ScheduleScoreQualityResult } from 'models/schedule/schedule.types';
|
import { ScheduleScoreQualityResponse, ScheduleScoreQualityResult } from 'models/schedule/schedule.types';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import { getScheduleQualityDetailsStyles } from './ScheduleQualityDetails.styles';
|
import { getScheduleQualityDetailsStyles } from './ScheduleQualityDetails.styles';
|
||||||
import { ScheduleQualityProgressBar } from './ScheduleQualityProgressBar';
|
import { ScheduleQualityProgressBar } from './ScheduleQualityProgressBar';
|
||||||
|
|
@ -112,19 +113,19 @@ export const ScheduleQualityDetails: FC<ScheduleQualityDetailsProps> = ({ qualit
|
||||||
bem(styles.container, 'withLateralPadding')
|
bem(styles.container, 'withLateralPadding')
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<HorizontalGroup spacing="sm">
|
<Stack gap={StackSize.sm}>
|
||||||
<Icon name="calculator-alt" />
|
<Icon name="calculator-alt" />
|
||||||
<Text type="secondary" className={styles.metholodogy}>
|
<Text type="secondary" className={styles.metholodogy}>
|
||||||
Calculation methodology
|
Calculation methodology
|
||||||
</Text>
|
</Text>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={expanded ? 'Collapse' : 'Expand'}
|
aria-label={expanded ? 'Collapse' : 'Expand'}
|
||||||
name={expanded ? 'arrow-down' : 'arrow-right'}
|
name={expanded ? 'arrow-down' : 'arrow-right'}
|
||||||
onClick={handleExpandClick}
|
onClick={handleExpandClick}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<Text type="primary" className={styles.text}>
|
<Text type="primary" className={styles.text}>
|
||||||
The next 52 weeks (~1 year) are taken into account when generating the quality report. Refer to the{' '}
|
The next 52 weeks (~1 year) are taken into account when generating the quality report. Refer to the{' '}
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,6 @@ describe('SourceCode', () => {
|
||||||
allBars.forEach((bar) => expect(bar.getAttribute('style').includes('width: 100%')));
|
allBars.forEach((bar) => expect(bar.getAttribute('style').includes('width: 100%')));
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each([0, 25, 30, 50, 65, 70, 100])('It renders at %p%', (completed) => {
|
|
||||||
const component = render(<ScheduleQualityProgressBar completed={completed} numTotalSteps={NUM_STEPS} />);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.each([0, 10, 19])('It renders as danger at <20% completion', (completed) => {
|
test.each([0, 10, 19])('It renders as danger at <20% completion', (completed) => {
|
||||||
render(<ScheduleQualityProgressBar completed={completed} numTotalSteps={NUM_STEPS} />);
|
render(<ScheduleQualityProgressBar completed={completed} numTotalSteps={NUM_STEPS} />);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,449 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`SourceCode It renders at 0% 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1ixaoku"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--danger"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--danger"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--danger"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--danger"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--danger"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`SourceCode It renders at 25% 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1ixaoku"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 25%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`SourceCode It renders at 30% 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1ixaoku"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 50%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`SourceCode It renders at 50% 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1ixaoku"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 50%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--warning"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`SourceCode It renders at 65% 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1ixaoku"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 25%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`SourceCode It renders at 70% 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1ixaoku"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 50%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 0%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`SourceCode It renders at 100% 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1ixaoku"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1oyrmju css-1oyrmju--progress"
|
|
||||||
data-testid="progressBar__row"
|
|
||||||
style="width: 20%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qn06wj css-qn06wj--primary"
|
|
||||||
data-testid="progressBar__bar"
|
|
||||||
style="width: 100%;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { FC, HTMLAttributes, ChangeEvent, useState, useCallback } from 'react';
|
import React, { FC, HTMLAttributes, ChangeEvent, useState, useCallback } from 'react';
|
||||||
|
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { IconButton, Modal, Input, HorizontalGroup, Button, VerticalGroup, useStyles2 } from '@grafana/ui';
|
import { IconButton, Modal, Input, Button, Stack, useStyles2 } from '@grafana/ui';
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
import { bem } from 'styles/utils.styles';
|
import { bem } from 'styles/utils.styles';
|
||||||
|
|
||||||
|
|
@ -138,7 +138,7 @@ export const Text: TextInterface = (props) => {
|
||||||
)}
|
)}
|
||||||
{isEditMode && (
|
{isEditMode && (
|
||||||
<Modal onDismiss={handleCancelEdit} closeOnEscape isOpen title={editModalTitle}>
|
<Modal onDismiss={handleCancelEdit} closeOnEscape isOpen title={editModalTitle}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
|
|
@ -149,15 +149,15 @@ export const Text: TextInterface = (props) => {
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
<HorizontalGroup justify="flex-end">
|
<Stack justifyContent="flex-end">
|
||||||
<Button variant="secondary" onClick={handleCancelEdit}>
|
<Button variant="secondary" onClick={handleCancelEdit}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" onClick={handleConfirmEdit}>
|
<Button variant="primary" onClick={handleConfirmEdit}>
|
||||||
Ok
|
Ok
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</CustomTag>
|
</CustomTag>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { HorizontalGroup, TimeOfDayPicker, useStyles2 } from '@grafana/ui';
|
import { Stack, TimeOfDayPicker, useStyles2 } from '@grafana/ui';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
interface TimeRangeProps {
|
interface TimeRangeProps {
|
||||||
|
|
@ -94,7 +94,7 @@ export const TimeRange = (props: TimeRangeProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.root, className)}>
|
<div className={cx(styles.root, className)}>
|
||||||
<HorizontalGroup wrap>
|
<Stack wrap="wrap">
|
||||||
<div data-testid="time-range-from">
|
<div data-testid="time-range-from">
|
||||||
{/* @ts-ignore actually TimeOfDayPicker uses Moment objects */}
|
{/* @ts-ignore actually TimeOfDayPicker uses Moment objects */}
|
||||||
<TimeOfDayPicker disabled={disabled} value={from} minuteStep={5} onChange={handleChangeFrom} />
|
<TimeOfDayPicker disabled={disabled} value={from} minuteStep={5} onChange={handleChangeFrom} />
|
||||||
|
|
@ -105,7 +105,7 @@ export const TimeRange = (props: TimeRangeProps) => {
|
||||||
<TimeOfDayPicker disabled={disabled} value={to} minuteStep={5} onChange={handleChangeTo} />
|
<TimeOfDayPicker disabled={disabled} value={to} minuteStep={5} onChange={handleChangeTo} />
|
||||||
</div>
|
</div>
|
||||||
{showNextDayTip && 'next day'}
|
{showNextDayTip && 'next day'}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { Icon, Tooltip, IconName, VerticalGroup, HorizontalGroup, useStyles2 } from '@grafana/ui';
|
import { Icon, Tooltip, IconName, Stack, useStyles2 } from '@grafana/ui';
|
||||||
import { bem } from 'styles/utils.styles';
|
import { bem } from 'styles/utils.styles';
|
||||||
|
|
||||||
import { Text, TextType } from 'components/Text/Text';
|
import { Text, TextType } from 'components/Text/Text';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import { getTooltipBadgeStyles } from './TooltipBadge.styles';
|
import { getTooltipBadgeStyles } from './TooltipBadge.styles';
|
||||||
|
|
||||||
|
|
@ -47,10 +48,10 @@ export const TooltipBadge: FC<TooltipBadgeProps> = (props) => {
|
||||||
interactive
|
interactive
|
||||||
content={
|
content={
|
||||||
<div className={styles.tooltip}>
|
<div className={styles.tooltip}>
|
||||||
<VerticalGroup spacing="xs">
|
<Stack direction="column" gap={StackSize.xs}>
|
||||||
<Text type="primary">{tooltipTitle}</Text>
|
<Text type="primary">{tooltipTitle}</Text>
|
||||||
{tooltipContent && <Text type="secondary">{tooltipContent}</Text>}
|
{tooltipContent && <Text type="secondary">{tooltipContent}</Text>}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -59,10 +60,10 @@ export const TooltipBadge: FC<TooltipBadgeProps> = (props) => {
|
||||||
onMouseEnter={onHover}
|
onMouseEnter={onHover}
|
||||||
{...(testId ? { 'data-testid': testId } : {})}
|
{...(testId ? { 'data-testid': testId } : {})}
|
||||||
>
|
>
|
||||||
<HorizontalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
{text !== undefined && <Text {...(testId ? { 'data-testid': `${testId}-text` } : {})}>{text}</Text>}
|
{text !== undefined && <Text {...(testId ? { 'data-testid': `${testId}-text` } : {})}>{text}</Text>}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { OrgRole } from '@grafana/data';
|
|
||||||
import { contextSrv } from 'grafana/app/core/core';
|
|
||||||
import renderer from 'react-test-renderer';
|
|
||||||
|
|
||||||
import { Unauthorized } from 'components/Unauthorized/Unauthorized';
|
|
||||||
import { getPluginId } from 'utils/consts';
|
|
||||||
|
|
||||||
jest.mock('grafana/app/core/core', () => ({
|
|
||||||
contextSrv: {
|
|
||||||
accessControlEnabled: (): boolean => null,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('Unauthorized', () => {
|
|
||||||
test.each([true, false])('renders properly - access control enabled: %s', (accessControlEnabled) => {
|
|
||||||
contextSrv.licensedAccessControlEnabled = () => accessControlEnabled;
|
|
||||||
const tree = renderer
|
|
||||||
.create(
|
|
||||||
<Unauthorized
|
|
||||||
requiredUserAction={{
|
|
||||||
permission: `${getPluginId()}.testing:hi`,
|
|
||||||
fallbackMinimumRoleRequired: OrgRole.Admin,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
.toJSON();
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.each([OrgRole.Admin, OrgRole.Editor, OrgRole.Viewer])(
|
|
||||||
'renders properly the grammar for different roles - %s',
|
|
||||||
(role) => {
|
|
||||||
contextSrv.licensedAccessControlEnabled = () => false;
|
|
||||||
const tree = renderer
|
|
||||||
.create(
|
|
||||||
<Unauthorized
|
|
||||||
requiredUserAction={{
|
|
||||||
permission: `${getPluginId()}.testing:hi`,
|
|
||||||
fallbackMinimumRoleRequired: role,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
.toJSON();
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -2,11 +2,12 @@ import React, { FC } from 'react';
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2, OrgRole } from '@grafana/data';
|
import { GrafanaTheme2, OrgRole } from '@grafana/data';
|
||||||
import { VerticalGroup, useStyles2 } from '@grafana/ui';
|
import { Stack, useStyles2 } from '@grafana/ui';
|
||||||
import { contextSrv } from 'grafana/app/core/core';
|
import { contextSrv } from 'grafana/app/core/core';
|
||||||
|
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
import { UserAction } from 'utils/authorization/authorization';
|
import { UserAction } from 'utils/authorization/authorization';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
requiredUserAction: UserAction;
|
requiredUserAction: UserAction;
|
||||||
|
|
@ -17,7 +18,7 @@ export const Unauthorized: FC<Props> = ({ requiredUserAction: { permission, fall
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.notFound}>
|
<div className={styles.notFound}>
|
||||||
<VerticalGroup spacing="lg" align="center">
|
<Stack direction="column" gap={StackSize.lg} alignItems="center">
|
||||||
<Text.Title level={1} className={styles.errorCode}>
|
<Text.Title level={1} className={styles.errorCode}>
|
||||||
403
|
403
|
||||||
</Text.Title>
|
</Text.Title>
|
||||||
|
|
@ -32,7 +33,7 @@ export const Unauthorized: FC<Props> = ({ requiredUserAction: { permission, fall
|
||||||
<br />
|
<br />
|
||||||
Please contact your organization administrator to request access.
|
Please contact your organization administrator to request access.
|
||||||
</Text.Title>
|
</Text.Title>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,291 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Unauthorized renders properly - access control enabled: false 1`] = `
|
|
||||||
<div
|
|
||||||
className="css-rs5aad"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="css-8tu8mo-vertical-group"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"height": "100%",
|
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="css-qxdyop-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
className="css-1fmhfo9"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"maxWidth": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
403
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="css-qxdyop-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h4
|
|
||||||
className="css-u023fv"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"maxWidth": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
You do not have access to view this page.
|
|
||||||
|
|
||||||
You must be at least an Admin.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Please contact your organization administrator to request access.
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Unauthorized renders properly - access control enabled: true 1`] = `
|
|
||||||
<div
|
|
||||||
className="css-rs5aad"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="css-8tu8mo-vertical-group"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"height": "100%",
|
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="css-qxdyop-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
className="css-1fmhfo9"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"maxWidth": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
403
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="css-qxdyop-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h4
|
|
||||||
className="css-u023fv"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"maxWidth": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
You do not have access to view this page.
|
|
||||||
|
|
||||||
You are missing the grafana-oncall-app.testing:hi permission.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Please contact your organization administrator to request access.
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Unauthorized renders properly the grammar for different roles - Admin 1`] = `
|
|
||||||
<div
|
|
||||||
className="css-rs5aad"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="css-8tu8mo-vertical-group"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"height": "100%",
|
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="css-qxdyop-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
className="css-1fmhfo9"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"maxWidth": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
403
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="css-qxdyop-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h4
|
|
||||||
className="css-u023fv"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"maxWidth": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
You do not have access to view this page.
|
|
||||||
|
|
||||||
You must be at least an Admin.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Please contact your organization administrator to request access.
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Unauthorized renders properly the grammar for different roles - Editor 1`] = `
|
|
||||||
<div
|
|
||||||
className="css-rs5aad"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="css-8tu8mo-vertical-group"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"height": "100%",
|
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="css-qxdyop-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
className="css-1fmhfo9"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"maxWidth": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
403
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="css-qxdyop-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h4
|
|
||||||
className="css-u023fv"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"maxWidth": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
You do not have access to view this page.
|
|
||||||
|
|
||||||
You must be at least an Editor.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Please contact your organization administrator to request access.
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Unauthorized renders properly the grammar for different roles - Viewer 1`] = `
|
|
||||||
<div
|
|
||||||
className="css-rs5aad"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="css-8tu8mo-vertical-group"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"height": "100%",
|
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="css-qxdyop-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h1
|
|
||||||
className="css-1fmhfo9"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"maxWidth": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
403
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="css-qxdyop-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h4
|
|
||||||
className="css-u023fv"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"maxWidth": undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
You do not have access to view this page.
|
|
||||||
|
|
||||||
You must be at least a Viewer.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Please contact your organization administrator to request access.
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { VerticalGroup, HorizontalGroup, IconButton, useStyles2 } from '@grafana/ui';
|
import { Stack, IconButton, useStyles2 } from '@grafana/ui';
|
||||||
import { arrayMoveImmutable } from 'array-move';
|
import { arrayMoveImmutable } from 'array-move';
|
||||||
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
|
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
|
||||||
import { bem } from 'styles/utils.styles';
|
import { bem } from 'styles/utils.styles';
|
||||||
|
|
@ -99,7 +99,7 @@ export const UserGroups = (props: UserGroupsProps) => {
|
||||||
{renderUser(item.data)}
|
{renderUser(item.data)}
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<div className={styles.userButtons}>
|
<div className={styles.userButtons}>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Remove"
|
aria-label="Remove"
|
||||||
className={styles.icon}
|
className={styles.icon}
|
||||||
|
|
@ -107,7 +107,7 @@ export const UserGroups = (props: UserGroupsProps) => {
|
||||||
onClick={getDeleteItemHandler(index)}
|
onClick={getDeleteItemHandler(index)}
|
||||||
/>
|
/>
|
||||||
<SortableHandleHoc />
|
<SortableHandleHoc />
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -115,7 +115,7 @@ export const UserGroups = (props: UserGroupsProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<RemoteSelect
|
<RemoteSelect
|
||||||
key={items.length}
|
key={items.length}
|
||||||
|
|
@ -142,7 +142,7 @@ export const UserGroups = (props: UserGroupsProps) => {
|
||||||
useDragHandle
|
useDragHandle
|
||||||
allowCreate={!disabled}
|
allowCreate={!disabled}
|
||||||
/>
|
/>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { FC, useMemo } from 'react';
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { VerticalGroup, HorizontalGroup, Badge, useStyles2, useTheme2 } from '@grafana/ui';
|
import { Stack, Badge, useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { SourceCode } from 'components/SourceCode/SourceCode';
|
import { SourceCode } from 'components/SourceCode/SourceCode';
|
||||||
|
|
@ -10,6 +10,7 @@ import { Tabs } from 'components/Tabs/Tabs';
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
|
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import { WebhookStatusCodeBadge } from './WebhookStatusCodeBadge';
|
import { WebhookStatusCodeBadge } from './WebhookStatusCodeBadge';
|
||||||
|
|
||||||
|
|
@ -41,14 +42,14 @@ export const WebhookLastEventDetails: FC<WebhookLastEventDetailsProps> = ({ webh
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.lastEventDetailsRowsWrapper}>
|
<div className={styles.lastEventDetailsRowsWrapper}>
|
||||||
<VerticalGroup spacing="md">
|
<Stack direction="column" gap={StackSize.md}>
|
||||||
{rows.map(({ title, value }) => (
|
{rows.map(({ title, value }) => (
|
||||||
<HorizontalGroup key={title}>
|
<Stack key={title}>
|
||||||
<span className={styles.lastEventDetailsRowTitle}>{title}</span>
|
<span className={styles.lastEventDetailsRowTitle}>{title}</span>
|
||||||
<span className={styles.lastEventDetailsRowValue}>{value}</span>
|
<span className={styles.lastEventDetailsRowValue}>{value}</span>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
<Tabs
|
||||||
queryStringKey="lastEventDetailsActiveTab"
|
queryStringKey="lastEventDetailsActiveTab"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useTheme2, useStyles2, HorizontalGroup, Button } from '@grafana/ui';
|
import { useTheme2, useStyles2, Button, Stack } from '@grafana/ui';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { Tag } from 'components/Tag/Tag';
|
import { Tag } from 'components/Tag/Tag';
|
||||||
|
|
@ -43,7 +43,7 @@ export const WebhookLastEventTimestamp = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Tag
|
<Tag
|
||||||
color={theme.colors.background.secondary}
|
color={theme.colors.background.secondary}
|
||||||
border={`1px solid ${theme.colors.border.weak}`}
|
border={`1px solid ${theme.colors.border.weak}`}
|
||||||
|
|
@ -61,7 +61,7 @@ export const WebhookLastEventTimestamp = ({
|
||||||
className={styles.eventDetailsIconButton}
|
className={styles.eventDetailsIconButton}
|
||||||
onClick={() => openDrawer('webhookDetails')}
|
onClick={() => openDrawer('webhookDetails')}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import { Provider } from 'mobx-react';
|
|
||||||
|
|
||||||
import { AddResponders } from './AddResponders';
|
|
||||||
|
|
||||||
jest.mock('./parts/AddRespondersPopup/AddRespondersPopup', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
AddRespondersPopup: () => <div>AddRespondersPopup</div>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('containers/WithPermissionControl/WithPermissionControlTooltip', () => ({
|
|
||||||
WithPermissionControlTooltip: ({ children }) => <div>{children}</div>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('AddResponders', () => {
|
|
||||||
const generateRemovePreviouslyPagedUserCallback = jest.fn();
|
|
||||||
|
|
||||||
test.each<'create' | 'update'>(['create', 'update'])('should render properly in %s mode', (mode) => {
|
|
||||||
const mockStoreValue = {
|
|
||||||
directPagingStore: {
|
|
||||||
selectedTeamResponder: null,
|
|
||||||
selectedUserResponders: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const component = render(
|
|
||||||
<Provider store={mockStoreValue}>
|
|
||||||
<AddResponders
|
|
||||||
mode={mode}
|
|
||||||
generateRemovePreviouslyPagedUserCallback={generateRemovePreviouslyPagedUserCallback}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.each([true, false])(
|
|
||||||
'should properly display the add responders button when hideAddResponderButton is %s',
|
|
||||||
(hideAddResponderButton) => {
|
|
||||||
const mockStoreValue = {
|
|
||||||
directPagingStore: {
|
|
||||||
selectedTeamResponder: null,
|
|
||||||
selectedUserResponders: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const component = render(
|
|
||||||
<Provider store={mockStoreValue}>
|
|
||||||
<AddResponders
|
|
||||||
mode="create"
|
|
||||||
hideAddResponderButton={hideAddResponderButton}
|
|
||||||
generateRemovePreviouslyPagedUserCallback={generateRemovePreviouslyPagedUserCallback}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
test('should render selected team and users properly', () => {
|
|
||||||
const mockStoreValue = {
|
|
||||||
directPagingStore: {
|
|
||||||
selectedTeamResponder: {
|
|
||||||
id: 'asdfasdf',
|
|
||||||
avatar_url: 'https://example.com',
|
|
||||||
name: 'my test team',
|
|
||||||
},
|
|
||||||
selectedUserResponders: [
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
pk: 'mcvnm',
|
|
||||||
avatar: 'https://example.com/user123.png',
|
|
||||||
username: 'my test user',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
pk: 'iuo',
|
|
||||||
avatar: 'https://example.com/user456.png',
|
|
||||||
username: 'my test user2',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const component = render(
|
|
||||||
<Provider store={mockStoreValue}>
|
|
||||||
<AddResponders
|
|
||||||
mode="create"
|
|
||||||
existingPagedUsers={[
|
|
||||||
{
|
|
||||||
pk: 'asdfasdf',
|
|
||||||
avatar: 'https://example.com/user9995.png',
|
|
||||||
username: 'my test user3',
|
|
||||||
} as any,
|
|
||||||
]}
|
|
||||||
generateRemovePreviouslyPagedUserCallback={generateRemovePreviouslyPagedUserCallback}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useCallback, useMemo } from 'react';
|
import React, { useState, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { HorizontalGroup, Button, Modal, Alert, VerticalGroup, Icon, useStyles2 } from '@grafana/ui';
|
import { Button, Modal, Alert, Stack, Icon, useStyles2 } from '@grafana/ui';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { UserHelper } from 'models/user/user.helpers';
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
import { UserActions } from 'utils/authorization/authorization';
|
import { UserActions } from 'utils/authorization/authorization';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import { getAddRespondersStyles } from './AddResponders.styles';
|
import { getAddRespondersStyles } from './AddResponders.styles';
|
||||||
import { NotificationPolicyValue, UserResponder as UserResponderType } from './AddResponders.types';
|
import { NotificationPolicyValue, UserResponder as UserResponderType } from './AddResponders.types';
|
||||||
|
|
@ -38,10 +39,10 @@ const LearnMoreAboutNotificationPoliciesLink: React.FC = () => {
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
<Text type="link">
|
<Text type="link">
|
||||||
<HorizontalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
Learn more
|
Learn more
|
||||||
<Icon name="external-link-alt" />
|
<Icon name="external-link-alt" />
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Text>
|
</Text>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
@ -111,7 +112,7 @@ export const AddResponders = observer(
|
||||||
<>
|
<>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Block bordered>
|
<Block bordered>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<Text.Title type="primary" level={4}>
|
<Text.Title type="primary" level={4}>
|
||||||
Participants
|
Participants
|
||||||
</Text.Title>
|
</Text.Title>
|
||||||
|
|
@ -128,7 +129,7 @@ export const AddResponders = observer(
|
||||||
</Button>
|
</Button>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
{(selectedTeamResponder || existingPagedUsers.length > 0 || selectedUserResponders.length > 0) && (
|
{(selectedTeamResponder || existingPagedUsers.length > 0 || selectedUserResponders.length > 0) && (
|
||||||
<>
|
<>
|
||||||
<ul className={styles.respondersList}>
|
<ul className={styles.respondersList}>
|
||||||
|
|
@ -189,7 +190,7 @@ export const AddResponders = observer(
|
||||||
onDismiss={closeUserConfirmationModal}
|
onDismiss={closeUserConfirmationModal}
|
||||||
className={styles.confirmParticipantInvitationModal}
|
className={styles.confirmParticipantInvitationModal}
|
||||||
>
|
>
|
||||||
<VerticalGroup spacing="md">
|
<Stack direction="column" gap={StackSize.md}>
|
||||||
{!isCreateMode && (
|
{!isCreateMode && (
|
||||||
<div>
|
<div>
|
||||||
<Text>
|
<Text>
|
||||||
|
|
@ -213,15 +214,15 @@ export const AddResponders = observer(
|
||||||
title="This user is not currently on-call. We don't recommend to page users outside on-call hours."
|
title="This user is not currently on-call. We don't recommend to page users outside on-call hours."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<HorizontalGroup justify="flex-end">
|
<Stack justifyContent="flex-end">
|
||||||
<Button variant="secondary" onClick={closeUserConfirmationModal}>
|
<Button variant="secondary" onClick={closeUserConfirmationModal}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" onClick={confirmCurrentlyConsideredUser} data-testid="confirm-non-oncall">
|
<Button variant="primary" onClick={confirmCurrentlyConsideredUser} data-testid="confirm-non-oncall">
|
||||||
Confirm
|
Confirm
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,672 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`AddResponders should properly display the add responders button when hideAddResponderButton is false 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1si66qn"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1mhys9y-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h4
|
|
||||||
class="css-u023fv"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
Participants
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
aria-disabled="false"
|
|
||||||
class="css-8b29hm-button"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1riaxdn"
|
|
||||||
>
|
|
||||||
Invite
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
AddRespondersPopup
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`AddResponders should properly display the add responders button when hideAddResponderButton is true 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1si66qn"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1mhys9y-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h4
|
|
||||||
class="css-u023fv"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
Participants
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
AddRespondersPopup
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`AddResponders should render properly in create mode 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1si66qn"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1mhys9y-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h4
|
|
||||||
class="css-u023fv"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
Participants
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
aria-disabled="false"
|
|
||||||
class="css-8b29hm-button"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1riaxdn"
|
|
||||||
>
|
|
||||||
Invite
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
AddRespondersPopup
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`AddResponders should render properly in update mode 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1si66qn"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1mhys9y-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h4
|
|
||||||
class="css-u023fv"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
Participants
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
aria-disabled="false"
|
|
||||||
class="css-8b29hm-button"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1riaxdn"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
AddRespondersPopup
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`AddResponders should render selected team and users properly 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1si66qn"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1mhys9y-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<h4
|
|
||||||
class="css-u023fv"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
Participants
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
aria-disabled="false"
|
|
||||||
class="css-8b29hm-button"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1riaxdn"
|
|
||||||
>
|
|
||||||
Invite
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="css-xp2upo"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
class="css-1mhys9y-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-ffyaiw-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1yiiywv"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="css-m6de9j css-m6de9j--medium"
|
|
||||||
data-testid="test__avatar"
|
|
||||||
src="https://example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-1dl45yk"
|
|
||||||
>
|
|
||||||
my test team
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Remove responder"
|
|
||||||
class="css-a2noi1"
|
|
||||||
data-testid="team-responder-delete-icon"
|
|
||||||
tabindex="0"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
class="css-1mhys9y-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-ffyaiw-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1yiiywv timeline-icon-background--green"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="css-m6de9j css-m6de9j--medium"
|
|
||||||
data-testid="test__avatar"
|
|
||||||
src="https://example.com/user9995.png"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-1dl45yk"
|
|
||||||
>
|
|
||||||
my test user3
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-ffyaiw-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-e49k3t-input-wrapper select css-8k5qe3-SelectContainer"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1f43avz-a11yText-A11yText"
|
|
||||||
id="react-select-2-live-region"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
aria-atomic="false"
|
|
||||||
aria-live="polite"
|
|
||||||
aria-relevant="additions text"
|
|
||||||
class="css-1f43avz-a11yText-A11yText"
|
|
||||||
role="log"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="css-1i88p6p"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1q0c0d5-grafana-select-value-container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class=" css-1n8tjau-placeholder"
|
|
||||||
id="react-select-2-placeholder"
|
|
||||||
>
|
|
||||||
Select...
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
aria-activedescendant=""
|
|
||||||
aria-autocomplete="list"
|
|
||||||
aria-describedby="react-select-2-placeholder"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-readonly="true"
|
|
||||||
class="css-mohuvp-dummyInput-DummyInput"
|
|
||||||
disabled=""
|
|
||||||
id="react-select-2-input"
|
|
||||||
inputmode="none"
|
|
||||||
role="combobox"
|
|
||||||
tabindex="0"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-zyjsuv-input-suffix"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Remove responder"
|
|
||||||
class="css-a2noi1"
|
|
||||||
data-testid="user-responder-delete-icon"
|
|
||||||
tabindex="0"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
class="css-1mhys9y-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-ffyaiw-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1yiiywv timeline-icon-background--green"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="css-m6de9j css-m6de9j--medium"
|
|
||||||
data-testid="test__avatar"
|
|
||||||
src="https://example.com/user123.png"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-1dl45yk"
|
|
||||||
>
|
|
||||||
my test user
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-ffyaiw-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1nmqu8c-input-wrapper select css-8k5qe3-SelectContainer"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1f43avz-a11yText-A11yText"
|
|
||||||
id="react-select-3-live-region"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
aria-atomic="false"
|
|
||||||
aria-live="polite"
|
|
||||||
aria-relevant="additions text"
|
|
||||||
class="css-1f43avz-a11yText-A11yText"
|
|
||||||
role="log"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="css-1i88p6p"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1q0c0d5-grafana-select-value-container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class=" css-1n8tjau-placeholder"
|
|
||||||
id="react-select-3-placeholder"
|
|
||||||
>
|
|
||||||
Select...
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
aria-activedescendant=""
|
|
||||||
aria-autocomplete="list"
|
|
||||||
aria-describedby="react-select-3-placeholder"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-readonly="true"
|
|
||||||
class="css-mohuvp-dummyInput-DummyInput"
|
|
||||||
id="react-select-3-input"
|
|
||||||
inputmode="none"
|
|
||||||
role="combobox"
|
|
||||||
tabindex="0"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-zyjsuv-input-suffix"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Remove responder"
|
|
||||||
class="css-a2noi1"
|
|
||||||
data-testid="user-responder-delete-icon"
|
|
||||||
tabindex="0"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
class="css-1mhys9y-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-ffyaiw-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1yiiywv timeline-icon-background--green"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="css-m6de9j css-m6de9j--medium"
|
|
||||||
data-testid="test__avatar"
|
|
||||||
src="https://example.com/user456.png"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-1dl45yk"
|
|
||||||
>
|
|
||||||
my test user2
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-ffyaiw-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1nmqu8c-input-wrapper select css-8k5qe3-SelectContainer"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1f43avz-a11yText-A11yText"
|
|
||||||
id="react-select-4-live-region"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
aria-atomic="false"
|
|
||||||
aria-live="polite"
|
|
||||||
aria-relevant="additions text"
|
|
||||||
class="css-1f43avz-a11yText-A11yText"
|
|
||||||
role="log"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="css-1i88p6p"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1q0c0d5-grafana-select-value-container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class=" css-1n8tjau-placeholder"
|
|
||||||
id="react-select-4-placeholder"
|
|
||||||
>
|
|
||||||
Select...
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
aria-activedescendant=""
|
|
||||||
aria-autocomplete="list"
|
|
||||||
aria-describedby="react-select-4-placeholder"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-readonly="true"
|
|
||||||
class="css-mohuvp-dummyInput-DummyInput"
|
|
||||||
id="react-select-4-input"
|
|
||||||
inputmode="none"
|
|
||||||
role="combobox"
|
|
||||||
tabindex="0"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-zyjsuv-input-suffix"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Remove responder"
|
|
||||||
class="css-a2noi1"
|
|
||||||
data-testid="user-responder-delete-icon"
|
|
||||||
tabindex="0"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<div
|
|
||||||
aria-label="[object Object]"
|
|
||||||
class="css-10yjoiw css-182y09v"
|
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1ewk8v0"
|
|
||||||
data-testid="data-testid Alert info"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-9n8jpb"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-tluiue"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-vjkmk1"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-b9x8ok"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="css-1cvxpvr"
|
|
||||||
href="https://grafana.com/docs/oncall/latest/notify/#configure-user-notification-policies"
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--link css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-ffyaiw-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-12kn7ff-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
Learn more
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12kn7ff-layoutChildrenWrapper"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
about Default vs Important user personal notification settings
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
AddRespondersPopup
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { render, waitFor } from '@testing-library/react';
|
|
||||||
import { Provider } from 'mobx-react';
|
|
||||||
|
|
||||||
import { UserHelper } from 'models/user/user.helpers';
|
|
||||||
|
|
||||||
import { AddRespondersPopup } from './AddRespondersPopup';
|
|
||||||
|
|
||||||
describe('AddRespondersPopup', () => {
|
|
||||||
const teams = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
avatar_url: 'https://example.com',
|
|
||||||
name: 'my test team',
|
|
||||||
number_of_users_currently_oncall: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
avatar_url: 'https://example.com',
|
|
||||||
name: 'my test team 2',
|
|
||||||
number_of_users_currently_oncall: 0,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
test('it shows a loading message initially', async () => {
|
|
||||||
const mockStoreValue = {
|
|
||||||
directPagingStore: {
|
|
||||||
selectedTeamResponder: null,
|
|
||||||
},
|
|
||||||
grafanaTeamStore: {
|
|
||||||
getSearchResult: jest.fn().mockReturnValue(teams),
|
|
||||||
updateItems: jest.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
UserHelper.search = jest.fn().mockReturnValue({ results: [] });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
const component = render(
|
|
||||||
<Provider store={mockStoreValue}>
|
|
||||||
<AddRespondersPopup
|
|
||||||
mode="create"
|
|
||||||
visible={true}
|
|
||||||
setVisible={jest.fn()}
|
|
||||||
setCurrentlyConsideredUser={jest.fn()}
|
|
||||||
setShowUserConfirmationModal={jest.fn()}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState, useCallback, useEffect, useRef, FC } from 'react';
|
import React, { useState, useCallback, useEffect, useRef, FC } from 'react';
|
||||||
|
|
||||||
import { Alert, HorizontalGroup, Icon, Input, LoadingPlaceholder, RadioButtonGroup } from '@grafana/ui';
|
import { Alert, Icon, Input, LoadingPlaceholder, RadioButtonGroup, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { ColumnsType } from 'rc-table/lib/interface';
|
import { ColumnsType } from 'rc-table/lib/interface';
|
||||||
|
|
@ -12,6 +12,7 @@ import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
|
||||||
import { UserHelper } from 'models/user/user.helpers';
|
import { UserHelper } from 'models/user/user.helpers';
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
import { useDebouncedCallback, useOnClickOutside } from 'utils/hooks';
|
import { useDebouncedCallback, useOnClickOutside } from 'utils/hooks';
|
||||||
|
|
||||||
import styles from './AddRespondersPopup.module.scss';
|
import styles from './AddRespondersPopup.module.scss';
|
||||||
|
|
@ -205,17 +206,17 @@ export const AddRespondersPopup = observer(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={() => addTeamResponder(team)} className={cx('responder-item')}>
|
<div onClick={() => addTeamResponder(team)} className={cx('responder-item')}>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Avatar size="small" src={avatar_url} />
|
<Avatar size="small" src={avatar_url} />
|
||||||
<Text>{name}</Text>
|
<Text>{name}</Text>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
{number_of_users_currently_oncall > 0 && (
|
{number_of_users_currently_oncall > 0 && (
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
{number_of_users_currently_oncall} user{number_of_users_currently_oncall > 1 ? 's' : ''} on-call
|
{number_of_users_currently_oncall} user{number_of_users_currently_oncall > 1 ? 's' : ''} on-call
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -233,20 +234,20 @@ export const AddRespondersPopup = observer(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={() => (disabled ? undefined : onClickUser(user))} className={cx('responder-item')}>
|
<div onClick={() => (disabled ? undefined : onClickUser(user))} className={cx('responder-item')}>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Avatar size="small" src={avatar} />
|
<Avatar size="small" src={avatar} />
|
||||||
<Text type={disabled ? 'disabled' : undefined} className={cx('responder-name')}>
|
<Text type={disabled ? 'disabled' : undefined} className={cx('responder-name')}>
|
||||||
{name || username}
|
{name || username}
|
||||||
</Text>
|
</Text>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
{/* TODO: we should add an elippsis and/or tooltip in the event that the user has a ton of teams */}
|
{/* TODO: we should add an elippsis and/or tooltip in the event that the user has a ton of teams */}
|
||||||
{teams?.length > 0 && (
|
{teams?.length > 0 && (
|
||||||
<Text type="secondary" className={cx('responder-team')}>
|
<Text type="secondary" className={cx('responder-team')}>
|
||||||
{teams.map(({ name }) => name).join(', ')}
|
{teams.map(({ name }) => name).join(', ')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -329,10 +330,10 @@ export const AddRespondersPopup = observer(
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
<Text type="link">
|
<Text type="link">
|
||||||
<HorizontalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
Learn more
|
Learn more
|
||||||
<Icon name="external-link-alt" />
|
<Icon name="external-link-alt" />
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Text>
|
</Text>
|
||||||
</a>
|
</a>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`AddRespondersPopup it shows a loading message initially 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="add-responders-dropdown"
|
|
||||||
data-testid="add-responders-popup"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-qfli5h-input-wrapper responders-filters"
|
|
||||||
data-testid="input-wrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-10lnb82-input-inputWrapper"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class="css-8tk2dk-input-input"
|
|
||||||
data-testid="add-responders-search-input"
|
|
||||||
placeholder="Search"
|
|
||||||
style="padding-right: 12px;"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="css-7099m8-input-suffix"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="radio-buttons css-1nxrz2e"
|
|
||||||
role="radiogroup"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1hvl7lx"
|
|
||||||
data-testid="data-testid radio-button"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
checked=""
|
|
||||||
class="css-18nv6l3"
|
|
||||||
id="option-teams-radiogroup-1"
|
|
||||||
name="radiogroup-1"
|
|
||||||
type="radio"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
class="css-18zk0h1"
|
|
||||||
for="option-teams-radiogroup-1"
|
|
||||||
>
|
|
||||||
Teams
|
|
||||||
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1hvl7lx"
|
|
||||||
data-testid="data-testid radio-button"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
class="css-18nv6l3"
|
|
||||||
id="option-users-radiogroup-1"
|
|
||||||
name="radiogroup-1"
|
|
||||||
type="radio"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
class="css-18zk0h1"
|
|
||||||
for="option-users-radiogroup-1"
|
|
||||||
>
|
|
||||||
Users
|
|
||||||
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1yjvs5a loading-placeholder"
|
|
||||||
>
|
|
||||||
Loading...
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="css-1baulvz"
|
|
||||||
data-testid="Spinner"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
|
|
||||||
import { NotificationPoliciesSelect } from './NotificationPoliciesSelect';
|
|
||||||
|
|
||||||
describe('NotificationPoliciesSelect', () => {
|
|
||||||
test('it renders properly', () => {
|
|
||||||
const component = render(<NotificationPoliciesSelect important={false} onChange={() => {}} />);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('disabled state', async () => {
|
|
||||||
const component = render(<NotificationPoliciesSelect disabled important={false} onChange={() => {}} />);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`NotificationPoliciesSelect disabled state 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-e49k3t-input-wrapper select css-8k5qe3-SelectContainer"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1f43avz-a11yText-A11yText"
|
|
||||||
id="react-select-3-live-region"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
aria-atomic="false"
|
|
||||||
aria-live="polite"
|
|
||||||
aria-relevant="additions text"
|
|
||||||
class="css-1f43avz-a11yText-A11yText"
|
|
||||||
role="log"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="css-1i88p6p"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1q0c0d5-grafana-select-value-container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-sr1xkh-singleValue css-upz218-SingleValue"
|
|
||||||
>
|
|
||||||
Default
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
aria-activedescendant=""
|
|
||||||
aria-autocomplete="list"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-readonly="true"
|
|
||||||
class="css-mohuvp-dummyInput-DummyInput"
|
|
||||||
disabled=""
|
|
||||||
id="react-select-3-input"
|
|
||||||
inputmode="none"
|
|
||||||
role="combobox"
|
|
||||||
tabindex="0"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-zyjsuv-input-suffix"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`NotificationPoliciesSelect it renders properly 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1nmqu8c-input-wrapper select css-8k5qe3-SelectContainer"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1f43avz-a11yText-A11yText"
|
|
||||||
id="react-select-2-live-region"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
aria-atomic="false"
|
|
||||||
aria-live="polite"
|
|
||||||
aria-relevant="additions text"
|
|
||||||
class="css-1f43avz-a11yText-A11yText"
|
|
||||||
role="log"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="css-1i88p6p"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1q0c0d5-grafana-select-value-container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-8nwx1l-singleValue css-upz218-SingleValue"
|
|
||||||
>
|
|
||||||
Default
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
aria-activedescendant=""
|
|
||||||
aria-autocomplete="list"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-readonly="true"
|
|
||||||
class="css-mohuvp-dummyInput-DummyInput"
|
|
||||||
id="react-select-2-input"
|
|
||||||
inputmode="none"
|
|
||||||
role="combobox"
|
|
||||||
tabindex="0"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-zyjsuv-input-suffix"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
@ -13,11 +13,6 @@ describe('TeamResponder', () => {
|
||||||
name: 'my test team',
|
name: 'my test team',
|
||||||
} as GrafanaTeam;
|
} as GrafanaTeam;
|
||||||
|
|
||||||
test('it renders data properly', () => {
|
|
||||||
const component = render(<TeamResponder team={team} handleDelete={() => {}} />);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it calls the delete callback', async () => {
|
test('it calls the delete callback', async () => {
|
||||||
const handleDelete = jest.fn();
|
const handleDelete = jest.fn();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { HorizontalGroup, IconButton, useStyles2 } from '@grafana/ui';
|
import { IconButton, Stack, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { Avatar } from 'components/Avatar/Avatar';
|
import { Avatar } from 'components/Avatar/Avatar';
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
|
|
@ -17,20 +17,20 @@ export const TeamResponder: FC<Props> = ({ team: { avatar_url, name }, handleDel
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<div className={styles.timelineIconBackground}>
|
<div className={styles.timelineIconBackground}>
|
||||||
<Avatar size="medium" src={avatar_url} />
|
<Avatar size="medium" src={avatar_url} />
|
||||||
</div>
|
</div>
|
||||||
<Text className={styles.responderName}>{name}</Text>
|
<Text className={styles.responderName}>{name}</Text>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<IconButton
|
<IconButton
|
||||||
data-testid="team-responder-delete-icon"
|
data-testid="team-responder-delete-icon"
|
||||||
tooltip="Remove responder"
|
tooltip="Remove responder"
|
||||||
name="trash-alt"
|
name="trash-alt"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`TeamResponder it renders data properly 1`] = `
|
|
||||||
<div>
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
class="css-1mhys9y-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-ffyaiw-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1yiiywv"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="css-m6de9j css-m6de9j--medium"
|
|
||||||
data-testid="test__avatar"
|
|
||||||
src="https://example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-1dl45yk"
|
|
||||||
>
|
|
||||||
my test team
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Remove responder"
|
|
||||||
class="css-a2noi1"
|
|
||||||
data-testid="team-responder-delete-icon"
|
|
||||||
tabindex="0"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
@ -13,13 +13,6 @@ describe('UserResponder', () => {
|
||||||
username: 'johnsmith',
|
username: 'johnsmith',
|
||||||
} as ApiSchemas['UserIsCurrentlyOnCall'];
|
} as ApiSchemas['UserIsCurrentlyOnCall'];
|
||||||
|
|
||||||
test('it renders data properly', () => {
|
|
||||||
const component = render(
|
|
||||||
<UserResponder important data={user} onImportantChange={() => {}} handleDelete={() => {}} />
|
|
||||||
);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it calls the delete callback', async () => {
|
test('it calls the delete callback', async () => {
|
||||||
const handleDelete = jest.fn();
|
const handleDelete = jest.fn();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { FC } from 'react';
|
||||||
|
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { ActionMeta, HorizontalGroup, IconButton, useStyles2 } from '@grafana/ui';
|
import { ActionMeta, IconButton, Stack, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { Avatar } from 'components/Avatar/Avatar';
|
import { Avatar } from 'components/Avatar/Avatar';
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
|
|
@ -27,14 +27,14 @@ export const UserResponder: FC<Props> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<div className={cx(styles.timelineIconBackground, { 'timeline-icon-background--green': true })}>
|
<div className={cx(styles.timelineIconBackground, { 'timeline-icon-background--green': true })}>
|
||||||
<Avatar size="medium" src={avatar} />
|
<Avatar size="medium" src={avatar} />
|
||||||
</div>
|
</div>
|
||||||
<Text className={styles.responderName}>{username}</Text>
|
<Text className={styles.responderName}>{username}</Text>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<NotificationPoliciesSelect
|
<NotificationPoliciesSelect
|
||||||
disabled={disableNotificationPolicySelect}
|
disabled={disableNotificationPolicySelect}
|
||||||
important={important}
|
important={important}
|
||||||
|
|
@ -46,8 +46,8 @@ export const UserResponder: FC<Props> = ({
|
||||||
name="trash-alt"
|
name="trash-alt"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`UserResponder it renders data properly 1`] = `
|
|
||||||
<div>
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
class="css-1mhys9y-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-ffyaiw-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1yiiywv timeline-icon-background--green"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="css-m6de9j css-m6de9j--medium"
|
|
||||||
data-testid="test__avatar"
|
|
||||||
src="http://avatar.com/"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--undefined css-77ouhj--medium css-1rchs6o--inline css-1dl45yk"
|
|
||||||
>
|
|
||||||
johnsmith
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-ffyaiw-horizontal-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1nmqu8c-input-wrapper select css-8k5qe3-SelectContainer"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1f43avz-a11yText-A11yText"
|
|
||||||
id="react-select-2-live-region"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
aria-atomic="false"
|
|
||||||
aria-live="polite"
|
|
||||||
aria-relevant="additions text"
|
|
||||||
class="css-1f43avz-a11yText-A11yText"
|
|
||||||
role="log"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="css-1i88p6p"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1q0c0d5-grafana-select-value-container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-8nwx1l-singleValue css-upz218-SingleValue"
|
|
||||||
>
|
|
||||||
Important
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
aria-activedescendant=""
|
|
||||||
aria-autocomplete="list"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-haspopup="true"
|
|
||||||
aria-readonly="true"
|
|
||||||
class="css-mohuvp-dummyInput-DummyInput"
|
|
||||||
id="react-select-2-input"
|
|
||||||
inputmode="none"
|
|
||||||
role="combobox"
|
|
||||||
tabindex="0"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-zyjsuv-input-suffix"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-18qv8yz-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Remove responder"
|
|
||||||
class="css-a2noi1"
|
|
||||||
data-testid="user-responder-delete-icon"
|
|
||||||
tabindex="0"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { VerticalGroup, useTheme2 } from '@grafana/ui';
|
import { Stack, useTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { Timeline } from 'components/Timeline/Timeline';
|
import { Timeline } from 'components/Timeline/Timeline';
|
||||||
import { MSTeamsConnector } from 'containers/AlertRules/parts/connectors/MSTeamsConnector';
|
import { MSTeamsConnector } from 'containers/AlertRules/parts/connectors/MSTeamsConnector';
|
||||||
|
|
@ -38,11 +38,11 @@ export const ChatOpsConnectors = (props: ChatOpsConnectorsProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Timeline.Item number={0} backgroundHexNumber={theme.colors.secondary.main} isDisabled={!showLineNumber}>
|
<Timeline.Item number={0} backgroundHexNumber={theme.colors.secondary.main} isDisabled={!showLineNumber}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
{isSlackInstalled && <SlackConnector channelFilterId={channelFilterId} />}
|
{isSlackInstalled && <SlackConnector channelFilterId={channelFilterId} />}
|
||||||
{isTelegramInstalled && <TelegramConnector channelFilterId={channelFilterId} />}
|
{isTelegramInstalled && <TelegramConnector channelFilterId={channelFilterId} />}
|
||||||
{isMSTeamsInstalled && <MSTeamsConnector channelFilterId={channelFilterId} />}
|
{isMSTeamsInstalled && <MSTeamsConnector channelFilterId={channelFilterId} />}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { HorizontalGroup, InlineSwitch } from '@grafana/ui';
|
import { InlineSwitch, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||||
import { MSTeamsChannel } from 'models/msteams_channel/msteams_channel.types';
|
import { MSTeamsChannel } from 'models/msteams_channel/msteams_channel.types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
import { UserActions } from 'utils/authorization/authorization';
|
import { UserActions } from 'utils/authorization/authorization';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import styles from 'containers/AlertRules/parts/connectors/Connectors.module.css';
|
import styles from 'containers/AlertRules/parts/connectors/Connectors.module.css';
|
||||||
|
|
||||||
|
|
@ -48,7 +49,7 @@ export const MSTeamsConnector = observer((props: MSTeamsConnectorProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx('root')}>
|
<div className={cx('root')}>
|
||||||
<HorizontalGroup wrap spacing="sm">
|
<Stack wrap="wrap" gap={StackSize.sm}>
|
||||||
<div className={cx('slack-channel-switch')}>
|
<div className={cx('slack-channel-switch')}>
|
||||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||||
<InlineSwitch
|
<InlineSwitch
|
||||||
|
|
@ -74,7 +75,7 @@ export const MSTeamsConnector = observer((props: MSTeamsConnectorProps) => {
|
||||||
onChange={handleMSTeamsChannelChange}
|
onChange={handleMSTeamsChannelChange}
|
||||||
/>
|
/>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { HorizontalGroup, InlineSwitch } from '@grafana/ui';
|
import { InlineSwitch, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config'
|
||||||
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
|
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
import { UserActions } from 'utils/authorization/authorization';
|
import { UserActions } from 'utils/authorization/authorization';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import styles from './Connectors.module.css';
|
import styles from './Connectors.module.css';
|
||||||
|
|
||||||
|
|
@ -45,7 +46,7 @@ export const SlackConnector = observer((props: SlackConnectorProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx('root')}>
|
<div className={cx('root')}>
|
||||||
<HorizontalGroup wrap spacing="sm">
|
<Stack wrap="wrap" gap={StackSize.sm}>
|
||||||
<div className={cx('slack-channel-switch')}>
|
<div className={cx('slack-channel-switch')}>
|
||||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||||
<InlineSwitch
|
<InlineSwitch
|
||||||
|
|
@ -74,7 +75,7 @@ export const SlackConnector = observer((props: SlackConnectorProps) => {
|
||||||
nullItemName={PRIVATE_CHANNEL_NAME}
|
nullItemName={PRIVATE_CHANNEL_NAME}
|
||||||
/>
|
/>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { HorizontalGroup, InlineSwitch } from '@grafana/ui';
|
import { InlineSwitch, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||||
import { TelegramChannel } from 'models/telegram_channel/telegram_channel.types';
|
import { TelegramChannel } from 'models/telegram_channel/telegram_channel.types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
import { UserActions } from 'utils/authorization/authorization';
|
import { UserActions } from 'utils/authorization/authorization';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import styles from './Connectors.module.css';
|
import styles from './Connectors.module.css';
|
||||||
|
|
||||||
|
|
@ -40,7 +41,7 @@ export const TelegramConnector = observer(({ channelFilterId }: TelegramConnecto
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx('root')}>
|
<div className={cx('root')}>
|
||||||
<HorizontalGroup wrap spacing="sm">
|
<Stack wrap="wrap" gap={StackSize.sm}>
|
||||||
<div className={cx('slack-channel-switch')}>
|
<div className={cx('slack-channel-switch')}>
|
||||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||||
<InlineSwitch
|
<InlineSwitch
|
||||||
|
|
@ -66,7 +67,7 @@ export const TelegramConnector = observer(({ channelFilterId }: TelegramConnecto
|
||||||
onChange={handleTelegramChannelChange}
|
onChange={handleTelegramChannelChange}
|
||||||
/>
|
/>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { HTMLAttributes, useState } from 'react';
|
import React, { HTMLAttributes, useState } from 'react';
|
||||||
|
|
||||||
import { Button, Field, HorizontalGroup, Input, Label, Modal, VerticalGroup } from '@grafana/ui';
|
import { Button, Field, Input, Label, Modal, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { get } from 'lodash-es';
|
import { get } from 'lodash-es';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
@ -48,7 +48,7 @@ export const ApiTokenForm = observer((props: TokenCreationModalProps) => {
|
||||||
<Modal isOpen closeOnEscape={false} title={token ? 'Your new API Token' : 'Create API Token'} onDismiss={onHide}>
|
<Modal isOpen closeOnEscape={false} title={token ? 'Your new API Token' : 'Create API Token'} onDismiss={onHide}>
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
<form onSubmit={handleSubmit(onCreateTokenCallback)}>
|
<form onSubmit={handleSubmit(onCreateTokenCallback)}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Label>Token Name</Label>
|
<Label>Token Name</Label>
|
||||||
<div className={cx('token__inputContainer')}>
|
<div className={cx('token__inputContainer')}>
|
||||||
{renderTokenInput()}
|
{renderTokenInput()}
|
||||||
|
|
@ -57,7 +57,7 @@ export const ApiTokenForm = observer((props: TokenCreationModalProps) => {
|
||||||
|
|
||||||
{renderCurlExample()}
|
{renderCurlExample()}
|
||||||
|
|
||||||
<HorizontalGroup justify="flex-end">
|
<Stack justifyContent="flex-end">
|
||||||
<Button variant="secondary" onClick={() => onHide()}>
|
<Button variant="secondary" onClick={() => onHide()}>
|
||||||
{token ? 'Close' : 'Cancel'}
|
{token ? 'Close' : 'Cancel'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -67,8 +67,8 @@ export const ApiTokenForm = observer((props: TokenCreationModalProps) => {
|
||||||
Create Token
|
Create Token
|
||||||
</Button>
|
</Button>
|
||||||
</RenderConditionally>
|
</RenderConditionally>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
@ -117,12 +117,12 @@ export const ApiTokenForm = observer((props: TokenCreationModalProps) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Label>Curl command example</Label>
|
<Label>Curl command example</Label>
|
||||||
<SourceCode noMinHeight showClipboardIconOnly>
|
<SourceCode noMinHeight showClipboardIconOnly>
|
||||||
{getCurlExample(token, store.pluginStore.apiUrlFromStatus)}
|
{getCurlExample(token, store.pluginStore.apiUrlFromStatus)}
|
||||||
</SourceCode>
|
</SourceCode>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Button, HorizontalGroup } from '@grafana/ui';
|
import { Button, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
|
|
@ -82,9 +82,9 @@ class _ApiTokenSettings extends React.Component<ApiTokensProps, any> {
|
||||||
<GTable
|
<GTable
|
||||||
title={() => (
|
title={() => (
|
||||||
<div className={cx('header')}>
|
<div className={cx('header')}>
|
||||||
<HorizontalGroup align="flex-end">
|
<Stack alignItems="flex-end">
|
||||||
<Text.Title level={3}>API Tokens</Text.Title>
|
<Text.Title level={3}>API Tokens</Text.Title>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<WithPermissionControlTooltip userAction={UserActions.APIKeysWrite}>
|
<WithPermissionControlTooltip userAction={UserActions.APIKeysWrite}>
|
||||||
<Button
|
<Button
|
||||||
icon="plus"
|
icon="plus"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { Button, Field, HorizontalGroup, Icon, Modal } from '@grafana/ui';
|
import { Button, Field, Icon, Modal, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
|
|
@ -65,10 +65,10 @@ export const AttachIncidentForm = observer(({ id, onUpdate, onHide }: AttachInci
|
||||||
isOpen
|
isOpen
|
||||||
icon="link"
|
icon="link"
|
||||||
title={
|
title={
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Icon size="lg" name="link" />
|
<Icon size="lg" name="link" />
|
||||||
<Text.Title level={4}>Attach to another alert group</Text.Title>
|
<Text.Title level={4}>Attach to another alert group</Text.Title>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
}
|
}
|
||||||
className={cx('root')}
|
className={cx('root')}
|
||||||
onDismiss={onHide}
|
onDismiss={onHide}
|
||||||
|
|
@ -97,14 +97,14 @@ export const AttachIncidentForm = observer(({ id, onUpdate, onHide }: AttachInci
|
||||||
/>
|
/>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</Field>
|
</Field>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Button onClick={onHide} variant="secondary">
|
<Button onClick={onHide} variant="secondary">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleLinkClick} variant="primary" disabled={!selected}>
|
<Button onClick={handleLinkClick} variant="primary" disabled={!selected}>
|
||||||
Attach
|
Attach
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,7 @@
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { LabelTag } from '@grafana/labels';
|
import { LabelTag } from '@grafana/labels';
|
||||||
import {
|
import { Button, Checkbox, IconButton, Input, LoadingPlaceholder, Modal, Stack, useStyles2 } from '@grafana/ui';
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
HorizontalGroup,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
LoadingPlaceholder,
|
|
||||||
Modal,
|
|
||||||
VerticalGroup,
|
|
||||||
useStyles2,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -26,7 +16,7 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import { components } from 'network/oncall-api/autogenerated-api.types';
|
import { components } from 'network/oncall-api/autogenerated-api.types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
import { UserActions } from 'utils/authorization/authorization';
|
import { UserActions } from 'utils/authorization/authorization';
|
||||||
import { PROCESSING_REQUEST_ERROR } from 'utils/consts';
|
import { PROCESSING_REQUEST_ERROR, StackSize } from 'utils/consts';
|
||||||
import { WrapWithGlobalNotification } from 'utils/decorators';
|
import { WrapWithGlobalNotification } from 'utils/decorators';
|
||||||
import { useDebouncedCallback, useIsLoading } from 'utils/hooks';
|
import { useDebouncedCallback, useIsLoading } from 'utils/hooks';
|
||||||
import { pluralize } from 'utils/utils';
|
import { pluralize } from 'utils/utils';
|
||||||
|
|
@ -68,9 +58,9 @@ export const ColumnsModal: React.FC<ColumnsModalProps> = observer(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isModalOpen} title={'Add column'} onDismiss={onCloseModal} closeOnEscape={false}>
|
<Modal isOpen={isModalOpen} title={'Add column'} onDismiss={onCloseModal} closeOnEscape={false}>
|
||||||
<VerticalGroup spacing="md">
|
<Stack direction="column" gap={StackSize.md}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<VerticalGroup spacing="md">
|
<Stack direction="column" gap={StackSize.md}>
|
||||||
<Input
|
<Input
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|
@ -87,9 +77,9 @@ export const ColumnsModal: React.FC<ColumnsModalProps> = observer(
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{inputRef?.current?.value && searchResults.length && (
|
{inputRef?.current?.value && searchResults.length && (
|
||||||
<VerticalGroup spacing="none">
|
<Stack direction="column" gap={StackSize.none}>
|
||||||
{searchResults.map((result, index) => (
|
{searchResults.map((result, index) => (
|
||||||
<VerticalGroup key={index}>
|
<Stack direction="column" key={index}>
|
||||||
<div className={styles.fieldRow}>
|
<div className={styles.fieldRow}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={result.isCollapsed ? 'Expand' : 'Collapse'}
|
aria-label={result.isCollapsed ? 'Expand' : 'Collapse'}
|
||||||
|
|
@ -122,18 +112,18 @@ export const ColumnsModal: React.FC<ColumnsModalProps> = observer(
|
||||||
)}
|
)}
|
||||||
</Block>
|
</Block>
|
||||||
)}
|
)}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{inputRef?.current?.value && searchResults.length === 0 && (
|
{inputRef?.current?.value && searchResults.length === 0 && (
|
||||||
<Text type="primary">0 results for your search.</Text>
|
<Text type="primary">0 results for your search.</Text>
|
||||||
)}
|
)}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HorizontalGroup justify="flex-end" spacing="md">
|
<Stack justifyContent="flex-end" gap={StackSize.md}>
|
||||||
<Button variant="secondary" onClick={onCloseModal}>
|
<Button variant="secondary" onClick={onCloseModal}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -149,19 +139,19 @@ export const ColumnsModal: React.FC<ColumnsModalProps> = observer(
|
||||||
{isLoading ? <LoadingPlaceholder className={cx('loadingPlaceholder')} text="Loading..." /> : 'Add'}
|
{isLoading ? <LoadingPlaceholder className={cx('loadingPlaceholder')} text="Loading..." /> : 'Add'}
|
||||||
</Button>
|
</Button>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
||||||
function renderLabelValues(keyName: string, values: Array<ApiSchemas['LabelValue']>) {
|
function renderLabelValues(keyName: string, values: Array<ApiSchemas['LabelValue']>) {
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
{values.slice(0, 2).map((val) => (
|
{values.slice(0, 2).map((val) => (
|
||||||
<LabelTag label={keyName} value={val.name} key={val.id} />
|
<LabelTag label={keyName} value={val.name} key={val.id} />
|
||||||
))}
|
))}
|
||||||
<div>{values.length > 2 ? `+ ${values.length - 2}` : ``}</div>
|
<div>{values.length > 2 ? `+ ${values.length - 2}` : ``}</div>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useStyles2, Button, HorizontalGroup, Icon, LoadingPlaceholder, Modal, VerticalGroup } from '@grafana/ui';
|
import { useStyles2, Button, Icon, LoadingPlaceholder, Modal, Stack } from '@grafana/ui';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
|
|
@ -13,7 +13,7 @@ import { ActionKey } from 'models/loader/action-keys';
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
import { UserActions } from 'utils/authorization/authorization';
|
import { UserActions } from 'utils/authorization/authorization';
|
||||||
import { PROCESSING_REQUEST_ERROR } from 'utils/consts';
|
import { PROCESSING_REQUEST_ERROR, StackSize } from 'utils/consts';
|
||||||
import { WrapAutoLoadingState, WrapWithGlobalNotification } from 'utils/decorators';
|
import { WrapAutoLoadingState, WrapWithGlobalNotification } from 'utils/decorators';
|
||||||
import { useIsLoading } from 'utils/hooks';
|
import { useIsLoading } from 'utils/hooks';
|
||||||
|
|
||||||
|
|
@ -68,10 +68,10 @@ export const ColumnsSelectorWrapper: React.FC<ColumnsSelectorWrapperProps> = obs
|
||||||
onDismiss={onConfirmRemovalClose}
|
onDismiss={onConfirmRemovalClose}
|
||||||
className={styles.removalModal}
|
className={styles.removalModal}
|
||||||
>
|
>
|
||||||
<VerticalGroup spacing="lg">
|
<Stack direction="column" gap={StackSize.lg}>
|
||||||
<Text type="primary">Are you sure you want to remove column {columnToBeRemoved?.name}?</Text>
|
<Text type="primary">Are you sure you want to remove column {columnToBeRemoved?.name}?</Text>
|
||||||
|
|
||||||
<HorizontalGroup justify="flex-end" spacing="md">
|
<Stack justifyContent="flex-end" gap={StackSize.md}>
|
||||||
<Button variant={'secondary'} onClick={onConfirmRemovalClose}>
|
<Button variant={'secondary'} onClick={onConfirmRemovalClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -90,8 +90,8 @@ export const ColumnsSelectorWrapper: React.FC<ColumnsSelectorWrapperProps> = obs
|
||||||
{isRemoveLoading ? <LoadingPlaceholder text="Loading..." className="loadingPlaceholder" /> : 'Remove'}
|
{isRemoveLoading ? <LoadingPlaceholder text="Loading..." className="loadingPlaceholder" /> : 'Remove'}
|
||||||
</Button>
|
</Button>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div ref={wrappingFloatingContainerRef}>
|
<div ref={wrappingFloatingContainerRef}>
|
||||||
|
|
@ -147,10 +147,10 @@ export const ColumnsSelectorWrapper: React.FC<ColumnsSelectorWrapperProps> = obs
|
||||||
id="toggletip-button"
|
id="toggletip-button"
|
||||||
onClick={() => setIsFloatingDisplayOpen(!isFloatingDisplayOpen)}
|
onClick={() => setIsFloatingDisplayOpen(!isFloatingDisplayOpen)}
|
||||||
>
|
>
|
||||||
<HorizontalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
Columns
|
Columns
|
||||||
<Icon name="angle-down" />
|
<Icon name="angle-down" />
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
|
|
||||||
import { HorizontalGroup, VerticalGroup, Modal, Tooltip, Icon, Button } from '@grafana/ui';
|
import { Stack, Modal, Tooltip, Icon, Button } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
@ -13,6 +13,7 @@ import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_re
|
||||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
import { openErrorNotification } from 'utils/utils';
|
import { openErrorNotification } from 'utils/utils';
|
||||||
|
|
||||||
import styles from './EditRegexpRouteTemplateModal.module.css';
|
import styles from './EditRegexpRouteTemplateModal.module.css';
|
||||||
|
|
@ -78,9 +79,9 @@ export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemp
|
||||||
title="Edit regular expression template"
|
title="Edit regular expression template"
|
||||||
className={cx('regexp-template-editor-modal')}
|
className={cx('regexp-template-editor-modal')}
|
||||||
>
|
>
|
||||||
<VerticalGroup spacing="lg">
|
<Stack direction="column" gap={StackSize.lg}>
|
||||||
<VerticalGroup spacing="xs">
|
<Stack direction="column" gap={StackSize.xs}>
|
||||||
<HorizontalGroup spacing={'xs'}>
|
<Stack gap={StackSize.xs}>
|
||||||
<Text type={'secondary'}>Regular expression</Text>
|
<Text type={'secondary'}>Regular expression</Text>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={'Use python style regex to filter incidents based on a expression'}
|
content={'Use python style regex to filter incidents based on a expression'}
|
||||||
|
|
@ -88,7 +89,7 @@ export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemp
|
||||||
>
|
>
|
||||||
<Icon name={'info-circle'} />
|
<Icon name={'info-circle'} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
|
|
||||||
<div className={cx('regexp-template-code', { 'regexp-template-code-error': showErrorTemplate })}>
|
<div className={cx('regexp-template-code', { 'regexp-template-code-error': showErrorTemplate })}>
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
|
|
@ -99,16 +100,16 @@ export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemp
|
||||||
onChange={handleRegexpBodyChange()}
|
onChange={handleRegexpBodyChange()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Text>Click "Convert to Jinja2" for a rich editor with debugger and additional functionality</Text>
|
<Text>Click "Convert to Jinja2" for a rich editor with debugger and additional functionality</Text>
|
||||||
<Text type={'secondary'}>Your template will be saved as the jinja2 template below</Text>
|
<Text type={'secondary'}>Your template will be saved as the jinja2 template below</Text>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
<Block bordered fullWidth withBackground>
|
<Block bordered fullWidth withBackground>
|
||||||
<Text type="link">{templateJinja2Body}</Text>
|
<Text type="link">{templateJinja2Body}</Text>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
<HorizontalGroup justify={'flex-end'}>
|
<Stack justifyContent={'flex-end'}>
|
||||||
<Button variant={'secondary'} onClick={onHide}>
|
<Button variant={'secondary'} onClick={onHide}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -118,8 +119,8 @@ export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemp
|
||||||
<Button variant={'primary'} onClick={() => handleSave()}>
|
<Button variant={'primary'} onClick={() => handleSave()}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { HorizontalGroup, VerticalGroup, Badge } from '@grafana/ui';
|
import { Stack, Badge } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Text } from 'components/Text/Text';
|
||||||
import { TeamName } from 'containers/TeamName/TeamName';
|
import { TeamName } from 'containers/TeamName/TeamName';
|
||||||
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
|
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import styles from './EscalationChainCard.module.css';
|
import styles from './EscalationChainCard.module.css';
|
||||||
|
|
||||||
|
|
@ -28,26 +29,31 @@ export const EscalationChainCard = observer((props: AlertReceiveChannelCardProps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx('root')}>
|
<div className={cx('root')}>
|
||||||
<HorizontalGroup align="flex-start">
|
<Stack alignItems="flex-start">
|
||||||
<VerticalGroup spacing="xs">
|
<Stack direction="column" gap={StackSize.xs}>
|
||||||
<HorizontalGroup spacing="sm">
|
<Stack gap={StackSize.sm}>
|
||||||
<Text type="primary" size="medium">
|
<Text type="primary" size="medium">
|
||||||
{escalationChain.name}
|
{escalationChain.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Badge
|
<div>
|
||||||
text={escalationChain.number_of_integrations}
|
<Badge
|
||||||
color="green"
|
text={escalationChain.number_of_integrations}
|
||||||
icon="link"
|
color="green"
|
||||||
tooltip={
|
icon="link"
|
||||||
escalationChain.number_of_integrations > 0 || escalationChain.number_of_routes > 0
|
tooltip={
|
||||||
? `Modifying this escalation chain will affect ${escalationChain.number_of_integrations} integrations and ${escalationChain.number_of_routes} routes.`
|
escalationChain.number_of_integrations > 0 || escalationChain.number_of_routes > 0
|
||||||
: 'This escalation is not connected to any integration route, go to integrations and connect route to this escalation chain'
|
? `Modifying this escalation chain will affect ${escalationChain.number_of_integrations} integrations and ${escalationChain.number_of_routes} routes.`
|
||||||
}
|
: 'This escalation is not connected to any integration route, go to integrations and connect route to this escalation chain'
|
||||||
/>
|
}
|
||||||
</HorizontalGroup>
|
/>
|
||||||
<TeamName team={grafanaTeamStore.items[escalationChain.team]} size="small" />
|
</div>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
|
||||||
|
<div>
|
||||||
|
<TeamName team={grafanaTeamStore.items[escalationChain.team]} size="small" />
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { Button, Field, HorizontalGroup, Input, Modal } from '@grafana/ui';
|
import { Button, Field, Input, Modal, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||||
|
|
@ -108,14 +108,14 @@ export const EscalationChainForm: FC<EscalationChainFormProps> = observer((props
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HorizontalGroup justify="flex-end">
|
<Stack justifyContent="flex-end">
|
||||||
<Button variant="secondary" onClick={onHide}>
|
<Button variant="secondary" onClick={onHide}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" variant="primary">
|
<Button type="submit" variant="primary">
|
||||||
{`${mode} Escalation Chain`}
|
{`${mode} Escalation Chain`}
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ interface GSelectProps<Item> {
|
||||||
openMenuOnFocus?: boolean;
|
openMenuOnFocus?: boolean;
|
||||||
width?: number | 'auto';
|
width?: number | 'auto';
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
dataTestId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GSelect = observer(<Item,>(props: GSelectProps<Item>) => {
|
export const GSelect = observer(<Item,>(props: GSelectProps<Item>) => {
|
||||||
|
|
@ -72,6 +73,7 @@ export const GSelect = observer(<Item,>(props: GSelectProps<Item>) => {
|
||||||
fetchItemFn,
|
fetchItemFn,
|
||||||
getSearchResult,
|
getSearchResult,
|
||||||
parseDisplayName,
|
parseDisplayName,
|
||||||
|
dataTestId = null,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const onChangeCallback = useCallback(
|
const onChangeCallback = useCallback(
|
||||||
|
|
@ -151,7 +153,7 @@ export const GSelect = observer(<Item,>(props: GSelectProps<Item>) => {
|
||||||
const Tag = isMulti ? AsyncMultiSelect : AsyncSelect;
|
const Tag = isMulti ? AsyncMultiSelect : AsyncSelect;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx('root', className)}>
|
<div className={cx('root', className)} data-testid={dataTestId}>
|
||||||
<Tag
|
<Tag
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
isSearchable
|
isSearchable
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { Button, HorizontalGroup, Icon, Label, Modal, Tooltip, VerticalGroup } from '@grafana/ui';
|
import { Button, Icon, Label, Modal, Tooltip, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -77,7 +77,7 @@ export const GrafanaTeamSelect = observer(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal onDismiss={onHide} closeOnEscape isOpen title="Select team" className={cx('root')}>
|
<Modal onDismiss={onHide} closeOnEscape isOpen title="Select team" className={cx('root')}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Label>
|
<Label>
|
||||||
<span className={cx('teamSelectText')}>
|
<span className={cx('teamSelectText')}>
|
||||||
Select team{''}
|
Select team{''}
|
||||||
|
|
@ -92,12 +92,12 @@ export const GrafanaTeamSelect = observer(
|
||||||
Edit teams
|
Edit teams
|
||||||
</a>
|
</a>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
<HorizontalGroup justify="flex-end">
|
<Stack justifyContent="flex-end">
|
||||||
<Button variant="primary" onClick={handleConfirm}>
|
<Button variant="primary" onClick={handleConfirm}>
|
||||||
Ok
|
Ok
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { ConfirmModal, HorizontalGroup, Icon, IconName } from '@grafana/ui';
|
import { ConfirmModal, Icon, IconName, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import { CommonIntegrationHelper } from 'pages/integration/CommonIntegration.helper';
|
import { CommonIntegrationHelper } from 'pages/integration/CommonIntegration.helper';
|
||||||
import { IntegrationHelper } from 'pages/integration/Integration.helper';
|
import { IntegrationHelper } from 'pages/integration/Integration.helper';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
const cx = cn.bind(styles);
|
||||||
|
|
||||||
|
|
@ -95,7 +96,7 @@ export const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRout
|
||||||
<div className={cx('collapsedRoute__container')}>
|
<div className={cx('collapsedRoute__container')}>
|
||||||
{chatOpsAvailableChannels.length > 0 && (
|
{chatOpsAvailableChannels.length > 0 && (
|
||||||
<div className={cx('collapsedRoute__item')}>
|
<div className={cx('collapsedRoute__item')}>
|
||||||
<HorizontalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
<Text type="secondary">Publish to ChatOps</Text>
|
<Text type="secondary">Publish to ChatOps</Text>
|
||||||
|
|
||||||
{chatOpsAvailableChannels.map(
|
{chatOpsAvailableChannels.map(
|
||||||
|
|
@ -109,7 +110,7 @@ export const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRout
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ import React, { useEffect, useReducer, useState } from 'react';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
HorizontalGroup,
|
Stack,
|
||||||
VerticalGroup,
|
|
||||||
Icon,
|
Icon,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
|
|
@ -44,6 +43,7 @@ import { MONACO_INPUT_HEIGHT_SMALL } from 'pages/integration/IntegrationCommon.c
|
||||||
import { AppFeature } from 'state/features';
|
import { AppFeature } from 'state/features';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
import { UserActions } from 'utils/authorization/authorization';
|
import { UserActions } from 'utils/authorization/authorization';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
import { openNotification } from 'utils/utils';
|
import { openNotification } from 'utils/utils';
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
const cx = cn.bind(styles);
|
||||||
|
|
@ -179,13 +179,13 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<VerticalGroup spacing="sm">
|
<Stack direction="column" gap={StackSize.sm}>
|
||||||
<Text customTag="h6" type="primary">
|
<Text customTag="h6" type="primary">
|
||||||
{hasLabels ? 'Alerts matched by' : 'Use routing template'}
|
{hasLabels ? 'Alerts matched by' : 'Use routing template'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<RenderConditionally shouldRender={hasLabels}>
|
<RenderConditionally shouldRender={hasLabels}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<div className={cx('labels-panel')}>
|
<div className={cx('labels-panel')}>
|
||||||
<RadioButtonGroup
|
<RadioButtonGroup
|
||||||
options={QueryBuilderOptions}
|
options={QueryBuilderOptions}
|
||||||
|
|
@ -195,7 +195,7 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RenderConditionally shouldRender={routingOption === RoutingOption.LABELS}>
|
<RenderConditionally shouldRender={routingOption === RoutingOption.LABELS}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<RouteLabelsDisplay labels={labels} onChange={onLabelsChange} labelErrors={labelErrors} />
|
<RouteLabelsDisplay labels={labels} onChange={onLabelsChange} labelErrors={labelErrors} />
|
||||||
|
|
||||||
<RenderConditionally shouldRender={shouldShowLabelAlert()}>
|
<RenderConditionally shouldRender={shouldShowLabelAlert()}>
|
||||||
|
|
@ -210,14 +210,14 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</RenderConditionally>
|
</RenderConditionally>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</RenderConditionally>
|
</RenderConditionally>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</RenderConditionally>
|
</RenderConditionally>
|
||||||
|
|
||||||
<RenderConditionally shouldRender={routingOption === RoutingOption.TEMPLATE || !hasLabels}>
|
<RenderConditionally shouldRender={routingOption === RoutingOption.TEMPLATE || !hasLabels}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<HorizontalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
<div className={cx('input', 'input--align')}>
|
<div className={cx('input', 'input--align')}>
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
value={channelFilterTemplate}
|
value={channelFilterTemplate}
|
||||||
|
|
@ -234,7 +234,7 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
||||||
size={'md'}
|
size={'md'}
|
||||||
onClick={() => handleEditRoutingTemplate(channelFilter, channelFilterId)}
|
onClick={() => handleEditRoutingTemplate(channelFilter, channelFilterId)}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<Alert
|
<Alert
|
||||||
severity="info"
|
severity="info"
|
||||||
title={
|
title={
|
||||||
|
|
@ -246,9 +246,9 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
||||||
) as unknown as string
|
) as unknown as string
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</RenderConditionally>
|
</RenderConditionally>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
@ -261,12 +261,12 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
||||||
canHoverIcon: false,
|
canHoverIcon: false,
|
||||||
expandedView: () => (
|
expandedView: () => (
|
||||||
<div className={cx('adjust-element-padding')}>
|
<div className={cx('adjust-element-padding')}>
|
||||||
<VerticalGroup spacing="sm">
|
<Stack direction="column" gap={StackSize.sm}>
|
||||||
<Text customTag="h6" type="primary">
|
<Text customTag="h6" type="primary">
|
||||||
Publish to ChatOps
|
Publish to ChatOps
|
||||||
</Text>
|
</Text>
|
||||||
<ChatOpsConnectors channelFilterId={channelFilterId} showLineNumber={false} />
|
<ChatOpsConnectors channelFilterId={channelFilterId} showLineNumber={false} />
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -279,13 +279,13 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
||||||
canHoverIcon: false,
|
canHoverIcon: false,
|
||||||
expandedView: () => (
|
expandedView: () => (
|
||||||
<div className={cx('adjust-element-padding')}>
|
<div className={cx('adjust-element-padding')}>
|
||||||
<VerticalGroup spacing="sm">
|
<Stack direction="column" gap={StackSize.sm}>
|
||||||
<Text customTag="h6" type="primary">
|
<Text customTag="h6" type="primary">
|
||||||
Trigger escalation chain
|
Trigger escalation chain
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div data-testid="escalation-chain-select">
|
<div data-testid="escalation-chain-select">
|
||||||
<HorizontalGroup spacing={'xs'}>
|
<Stack gap={StackSize.xs}>
|
||||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||||
<Select
|
<Select
|
||||||
isClearable
|
isClearable
|
||||||
|
|
@ -336,19 +336,19 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
||||||
variant={'secondary'}
|
variant={'secondary'}
|
||||||
onClick={() => setState({ isEscalationCollapsed: !isEscalationCollapsed })}
|
onClick={() => setState({ isEscalationCollapsed: !isEscalationCollapsed })}
|
||||||
>
|
>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Text type="link">{isEscalationCollapsed ? 'Show' : 'Hide'} escalation chain</Text>
|
<Text type="link">{isEscalationCollapsed ? 'Show' : 'Hide'} escalation chain</Text>
|
||||||
{isEscalationCollapsed && <Icon name={'angle-right'} />}
|
{isEscalationCollapsed && <Icon name={'angle-right'} />}
|
||||||
{!isEscalationCollapsed && <Icon name={'angle-up'} />}
|
{!isEscalationCollapsed && <Icon name={'angle-up'} />}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
{!isEscalationCollapsed && (
|
{!isEscalationCollapsed && (
|
||||||
<ReadOnlyEscalationChain escalationChainId={channelFilter.escalation_chain} />
|
<ReadOnlyEscalationChain escalationChainId={channelFilter.escalation_chain} />
|
||||||
)}
|
)}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
@ -506,7 +506,7 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
||||||
const channelFilterIds = alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId];
|
const channelFilterIds = alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup spacing={'xs'}>
|
<Stack gap={StackSize.xs}>
|
||||||
{routeIndex > 0 && !channelFilter.is_default && (
|
{routeIndex > 0 && !channelFilter.is_default && (
|
||||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||||
<Tooltip placement="top" content={'Move Up'}>
|
<Tooltip placement="top" content={'Move Up'}>
|
||||||
|
|
@ -533,11 +533,11 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
||||||
|
|
||||||
<CopyToClipboard text={channelFilter.id} onCopy={() => openNotification('Route ID is copied')}>
|
<CopyToClipboard text={channelFilter.id} onCopy={() => openNotification('Route ID is copied')}>
|
||||||
<div className={cx('integrations-actionItem')}>
|
<div className={cx('integrations-actionItem')}>
|
||||||
<HorizontalGroup spacing={'xs'}>
|
<Stack gap={StackSize.xs}>
|
||||||
<Icon name="copy" />
|
<Icon name="copy" />
|
||||||
|
|
||||||
<Text type="primary">UID: {channelFilter.id}</Text>
|
<Text type="primary">UID: {channelFilter.id}</Text>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
|
|
||||||
|
|
@ -546,10 +546,10 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
||||||
<WithPermissionControlTooltip key="delete" userAction={UserActions.IntegrationsWrite}>
|
<WithPermissionControlTooltip key="delete" userAction={UserActions.IntegrationsWrite}>
|
||||||
<div className={cx('integrations-actionItem')} onClick={onDelete}>
|
<div className={cx('integrations-actionItem')} onClick={onDelete}>
|
||||||
<Text type="danger">
|
<Text type="danger">
|
||||||
<HorizontalGroup spacing={'xs'}>
|
<Stack gap={StackSize.xs}>
|
||||||
<Icon name="trash-alt" />
|
<Icon name="trash-alt" />
|
||||||
<span>Delete Route</span>
|
<span>Delete Route</span>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
|
|
@ -567,7 +567,7 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
||||||
)}
|
)}
|
||||||
</WithContextMenu>
|
</WithContextMenu>
|
||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
||||||
function onDelete() {
|
function onDelete() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { Button, Drawer, Field, HorizontalGroup, Icon, Select, VerticalGroup } from '@grafana/ui';
|
import { Button, Drawer, Field, Icon, Select, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { SelectOption } from 'state/types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
import { withMobXProviderContext } from 'state/withStore';
|
import { withMobXProviderContext } from 'state/withStore';
|
||||||
import { UserActions } from 'utils/authorization/authorization';
|
import { UserActions } from 'utils/authorization/authorization';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
import { openNotification } from 'utils/utils';
|
import { openNotification } from 'utils/utils';
|
||||||
|
|
||||||
import styles from './IntegrationHeartbeatForm.module.scss';
|
import styles from './IntegrationHeartbeatForm.module.scss';
|
||||||
|
|
@ -47,14 +48,14 @@ const _IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: I
|
||||||
return (
|
return (
|
||||||
<Drawer width={'640px'} scrollableContent title={'Heartbeat'} onClose={onClose} closeOnMaskClick={false}>
|
<Drawer width={'640px'} scrollableContent title={'Heartbeat'} onClose={onClose} closeOnMaskClick={false}>
|
||||||
<div data-testid="heartbeat-settings-form">
|
<div data-testid="heartbeat-settings-form">
|
||||||
<VerticalGroup spacing={'lg'}>
|
<Stack direction="column" gap={StackSize.lg}>
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
A heartbeat acts as a healthcheck for alert group monitoring. You can configure you monitoring to regularly
|
A heartbeat acts as a healthcheck for alert group monitoring. You can configure you monitoring to regularly
|
||||||
send alerts to the heartbeat endpoint. If OnCall doesn't receive one of these alerts, it will create an new
|
send alerts to the heartbeat endpoint. If OnCall doesn't receive one of these alerts, it will create an new
|
||||||
alert group and escalate it
|
alert group and escalate it
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<VerticalGroup spacing="md">
|
<Stack direction="column" gap={StackSize.md}>
|
||||||
<div className={cx('u-width-100')}>
|
<div className={cx('u-width-100')}>
|
||||||
<Field label={'Setup heartbeat interval'}>
|
<Field label={'Setup heartbeat interval'}>
|
||||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||||
|
|
@ -83,16 +84,17 @@ const _IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: I
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
<Text type="link" size="small">
|
<Text type="link" size="small">
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
How to configure heartbeats
|
How to configure heartbeats
|
||||||
<Icon name="external-link-alt" />
|
<Icon name="external-link-alt" />
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Text>
|
</Text>
|
||||||
</a>
|
</a>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
|
|
||||||
<VerticalGroup style={{ marginTop: 'auto' }}>
|
{/* TODO: Check if the styles were appended previously */}
|
||||||
<HorizontalGroup className={cx('buttons')} justify="flex-end">
|
<Stack direction="column">
|
||||||
|
<Stack justifyContent="flex-end">
|
||||||
<Button variant={'secondary'} onClick={onClose} data-testid="close-heartbeat-form">
|
<Button variant={'secondary'} onClick={onClose} data-testid="close-heartbeat-form">
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -108,9 +110,9 @@ const _IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: I
|
||||||
</Button>
|
</Button>
|
||||||
</WithConfirm>
|
</WithConfirm>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { SelectableValue } from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Field,
|
Field,
|
||||||
HorizontalGroup,
|
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
|
|
@ -14,7 +13,7 @@ import {
|
||||||
Switch,
|
Switch,
|
||||||
TextArea,
|
TextArea,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
VerticalGroup,
|
Stack,
|
||||||
useStyles2,
|
useStyles2,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
@ -37,7 +36,13 @@ import { IntegrationHelper, getIsBidirectionalIntegration } from 'pages/integrat
|
||||||
import { AppFeature } from 'state/features';
|
import { AppFeature } from 'state/features';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
import { UserActions } from 'utils/authorization/authorization';
|
import { UserActions } from 'utils/authorization/authorization';
|
||||||
import { PLUGIN_ROOT, generateAssignToTeamInputDescription, DOCS_ROOT, INTEGRATION_SERVICENOW } from 'utils/consts';
|
import {
|
||||||
|
PLUGIN_ROOT,
|
||||||
|
generateAssignToTeamInputDescription,
|
||||||
|
DOCS_ROOT,
|
||||||
|
INTEGRATION_SERVICENOW,
|
||||||
|
StackSize,
|
||||||
|
} from 'utils/consts';
|
||||||
import { useIsLoading } from 'utils/hooks';
|
import { useIsLoading } from 'utils/hooks';
|
||||||
import { validateURL } from 'utils/string';
|
import { validateURL } from 'utils/string';
|
||||||
import { OmitReadonlyMembers } from 'utils/types';
|
import { OmitReadonlyMembers } from 'utils/types';
|
||||||
|
|
@ -296,17 +301,17 @@ export const IntegrationForm = observer(
|
||||||
|
|
||||||
<RenderConditionally shouldRender={isServiceNow && isNew}>
|
<RenderConditionally shouldRender={isServiceNow && isNew}>
|
||||||
<div className={styles.serviceNowHeading}>
|
<div className={styles.serviceNowHeading}>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Text type="primary">ServiceNow configuration</Text>
|
<Text type="primary">ServiceNow configuration</Text>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Text type={'primary'} size={'small'}>
|
<Text type={'primary'} size={'small'}>
|
||||||
Fill in ServiceNow credentials to be used by Grafana OnCall.{' '}
|
Fill in ServiceNow credentials to be used by Grafana OnCall.{' '}
|
||||||
<a href={`${DOCS_ROOT}/integrations/servicenow/`} target="_blank" rel="noreferrer">
|
<a href={`${DOCS_ROOT}/integrations/servicenow/`} target="_blank" rel="noreferrer">
|
||||||
<Text type="link">Read setup guide</Text>
|
<Text type="link">Read setup guide</Text>
|
||||||
</a>
|
</a>
|
||||||
</Text>
|
</Text>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
|
|
@ -382,7 +387,7 @@ export const IntegrationForm = observer(
|
||||||
</RenderConditionally>
|
</RenderConditionally>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<HorizontalGroup justify="flex-end">
|
<Stack justifyContent="flex-end">
|
||||||
{id === 'new' ? (
|
{id === 'new' ? (
|
||||||
<Button variant="secondary" onClick={onBackClick}>
|
<Button variant="secondary" onClick={onBackClick}>
|
||||||
Back
|
Back
|
||||||
|
|
@ -396,7 +401,7 @@ export const IntegrationForm = observer(
|
||||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||||
{renderUpdateIntegrationButton(id)}
|
{renderUpdateIntegrationButton(id)}
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
@ -543,13 +548,13 @@ const GrafanaContactPoint = observer(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.extraFields}>
|
<div className={styles.extraFields}>
|
||||||
<VerticalGroup spacing="md">
|
<Stack direction="column" gap={StackSize.md}>
|
||||||
<HorizontalGroup spacing="xs" align="center">
|
<Stack gap={StackSize.xs} alignItems="center">
|
||||||
<Text type="primary" size="small">
|
<Text type="primary" size="small">
|
||||||
Grafana Alerting Contact point
|
Grafana Alerting Contact point
|
||||||
</Text>
|
</Text>
|
||||||
<Icon name="info-circle" />
|
<Icon name="info-circle" />
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
|
|
||||||
<div className={styles.extraFieldsRadio}>
|
<div className={styles.extraFieldsRadio}>
|
||||||
<Controller
|
<Controller
|
||||||
|
|
@ -625,7 +630,7 @@ const GrafanaContactPoint = observer(
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState, ChangeEvent } from 'react';
|
import React, { useState, ChangeEvent } from 'react';
|
||||||
|
|
||||||
import { Drawer, VerticalGroup, HorizontalGroup, Input, Tag, EmptySearchResult } from '@grafana/ui';
|
import { Drawer, Stack, Input, Tag, EmptySearchResult } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { IntegrationLogo } from 'components/IntegrationLogo/IntegrationLogo';
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import { IntegrationForm } from './IntegrationForm';
|
import { IntegrationForm } from './IntegrationForm';
|
||||||
import styles from './IntegrationFormContainer.module.scss';
|
import styles from './IntegrationFormContainer.module.scss';
|
||||||
|
|
@ -59,7 +60,7 @@ export const IntegrationFormContainer = observer((props: IntegrationFormContaine
|
||||||
{showIntegrationsListDrawer && (
|
{showIntegrationsListDrawer && (
|
||||||
<Drawer scrollableContent title="New Integration" onClose={onHide} closeOnMaskClick={false} width="640px">
|
<Drawer scrollableContent title="New Integration" onClose={onHide} closeOnMaskClick={false} width="640px">
|
||||||
<div className={cx('content')}>
|
<div className={cx('content')}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
Integration receives alerts on an unique API URL, interprets them using set of templates tailored for
|
Integration receives alerts on an unique API URL, interprets them using set of templates tailored for
|
||||||
monitoring system and starts escalations.
|
monitoring system and starts escalations.
|
||||||
|
|
@ -75,14 +76,14 @@ export const IntegrationFormContainer = observer((props: IntegrationFormContaine
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IntegrationBlocks options={options} onBlockClick={onBlockClick} />
|
<IntegrationBlocks options={options} onBlockClick={onBlockClick} />
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)}
|
)}
|
||||||
{(showNewIntegrationForm || !showIntegrationsListDrawer) && (
|
{(showNewIntegrationForm || !showIntegrationsListDrawer) && (
|
||||||
<Drawer scrollableContent title={getTitle()} onClose={onHide} closeOnMaskClick={false} width="640px">
|
<Drawer scrollableContent title={getTitle()} onClose={onHide} closeOnMaskClick={false} width="640px">
|
||||||
<div className={cx('content')}>
|
<div className={cx('content')}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<IntegrationForm
|
<IntegrationForm
|
||||||
id={id}
|
id={id}
|
||||||
onBackClick={onBackClick}
|
onBackClick={onBackClick}
|
||||||
|
|
@ -91,7 +92,7 @@ export const IntegrationFormContainer = observer((props: IntegrationFormContaine
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onHide={onHide}
|
onHide={onHide}
|
||||||
/>
|
/>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)}
|
)}
|
||||||
|
|
@ -139,19 +140,19 @@ const IntegrationBlocks: React.FC<{
|
||||||
<IntegrationLogo integration={alertReceiveChannelChoice} scale={0.2} />
|
<IntegrationLogo integration={alertReceiveChannelChoice} scale={0.2} />
|
||||||
</div>
|
</div>
|
||||||
<div className={cx('title')}>
|
<div className={cx('title')}>
|
||||||
<VerticalGroup spacing={alertReceiveChannelChoice.featured ? 'xs' : 'none'}>
|
<Stack direction="column" gap={alertReceiveChannelChoice.featured ? StackSize.xs : StackSize.none}>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Text strong data-testid="integration-display-name">
|
<Text strong data-testid="integration-display-name">
|
||||||
{alertReceiveChannelChoice.display_name}
|
{alertReceiveChannelChoice.display_name}
|
||||||
</Text>
|
</Text>
|
||||||
{alertReceiveChannelChoice.featured && alertReceiveChannelChoice.featured_tag_name && (
|
{alertReceiveChannelChoice.featured && alertReceiveChannelChoice.featured_tag_name && (
|
||||||
<Tag name={alertReceiveChannelChoice.featured_tag_name} colorIndex={5} />
|
<Tag name={alertReceiveChannelChoice.featured_tag_name} colorIndex={5} />
|
||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<Text type="secondary" size="small">
|
<Text type="secondary" size="small">
|
||||||
{alertReceiveChannelChoice.short_description}
|
{alertReceiveChannelChoice.short_description}
|
||||||
</Text>
|
</Text>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,7 @@
|
||||||
import React, { ChangeEvent, useState } from 'react';
|
import React, { ChangeEvent, useState } from 'react';
|
||||||
|
|
||||||
import { ServiceLabels } from '@grafana/labels';
|
import { ServiceLabels } from '@grafana/labels';
|
||||||
import {
|
import { Alert, Button, Drawer, Dropdown, InlineSwitch, Input, Menu, Stack } from '@grafana/ui';
|
||||||
Alert,
|
|
||||||
Button,
|
|
||||||
Drawer,
|
|
||||||
Dropdown,
|
|
||||||
HorizontalGroup,
|
|
||||||
InlineSwitch,
|
|
||||||
Input,
|
|
||||||
Menu,
|
|
||||||
VerticalGroup,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -26,7 +16,7 @@ import { LabelsErrors } from 'models/label/label.types';
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import { LabelTemplateOptions } from 'pages/integration/IntegrationCommon.config';
|
import { LabelTemplateOptions } from 'pages/integration/IntegrationCommon.config';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
import { DOCS_ROOT, GENERIC_ERROR } from 'utils/consts';
|
import { DOCS_ROOT, GENERIC_ERROR, StackSize } from 'utils/consts';
|
||||||
import { openErrorNotification } from 'utils/utils';
|
import { openErrorNotification } from 'utils/utils';
|
||||||
|
|
||||||
import { getIsAddBtnDisabled, getIsTooManyLabelsWarningVisible } from './IntegrationLabelsForm.helpers';
|
import { getIsAddBtnDisabled, getIsTooManyLabelsWarningVisible } from './IntegrationLabelsForm.helpers';
|
||||||
|
|
@ -105,7 +95,7 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
||||||
closeOnMaskClick={false}
|
closeOnMaskClick={false}
|
||||||
width="640px"
|
width="640px"
|
||||||
>
|
>
|
||||||
<VerticalGroup spacing="lg">
|
<Stack direction="column" gap={StackSize.lg}>
|
||||||
<RenderConditionally shouldRender={getIsTooManyLabelsWarningVisible(alertGroupLabels)}>
|
<RenderConditionally shouldRender={getIsTooManyLabelsWarningVisible(alertGroupLabels)}>
|
||||||
<Alert title="More than 15 labels added" severity="warning">
|
<Alert title="More than 15 labels added" severity="warning">
|
||||||
We support up to 15 labels per Alert group. Please remove extra labels.
|
We support up to 15 labels per Alert group. Please remove extra labels.
|
||||||
|
|
@ -113,10 +103,10 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
||||||
Otherwise, only the first 15 labels (alphabetically sorted by keys) will be applied.
|
Otherwise, only the first 15 labels (alphabetically sorted by keys) will be applied.
|
||||||
</Alert>
|
</Alert>
|
||||||
</RenderConditionally>
|
</RenderConditionally>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Text>Integration labels</Text>
|
<Text>Integration labels</Text>
|
||||||
{alertReceiveChannel.labels.length ? (
|
{alertReceiveChannel.labels.length ? (
|
||||||
<VerticalGroup spacing="xs">
|
<Stack direction="column" gap={StackSize.xs}>
|
||||||
<Text type="secondary" size="small">
|
<Text type="secondary" size="small">
|
||||||
Labels inherited from <PluginLink onClick={handleOpenIntegrationSettings}>the integration</PluginLink>
|
Labels inherited from <PluginLink onClick={handleOpenIntegrationSettings}>the integration</PluginLink>
|
||||||
. This behavior can be disabled using the toggle option.
|
. This behavior can be disabled using the toggle option.
|
||||||
|
|
@ -124,7 +114,7 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
||||||
<ul className={cx('labels-list')}>
|
<ul className={cx('labels-list')}>
|
||||||
{alertReceiveChannel.labels.map((label) => (
|
{alertReceiveChannel.labels.map((label) => (
|
||||||
<li key={label.key.id}>
|
<li key={label.key.id}>
|
||||||
<HorizontalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
<Input width={INPUT_WIDTH / 8} value={label.key.name} disabled />
|
<Input width={INPUT_WIDTH / 8} value={label.key.name} disabled />
|
||||||
<Input width={INPUT_WIDTH / 8} value={label.value.name} disabled />
|
<Input width={INPUT_WIDTH / 8} value={label.value.name} disabled />
|
||||||
<InlineSwitch
|
<InlineSwitch
|
||||||
|
|
@ -132,20 +122,20 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
||||||
transparent
|
transparent
|
||||||
onChange={() => onInheritanceChange(label.key.id)}
|
onChange={() => onInheritanceChange(label.key.id)}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Text type="secondary">There are no labels to inherit yet</Text>
|
<Text type="secondary">There are no labels to inherit yet</Text>
|
||||||
<Text type="link" onClick={handleOpenIntegrationSettings} clickable>
|
<Text type="link" onClick={handleOpenIntegrationSettings} clickable>
|
||||||
Add labels to the integration
|
Add labels to the integration
|
||||||
</Text>
|
</Text>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
|
|
||||||
<CustomLabels
|
<CustomLabels
|
||||||
alertGroupLabels={alertGroupLabels}
|
alertGroupLabels={alertGroupLabels}
|
||||||
|
|
@ -158,8 +148,8 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Collapse isOpen={false} label="Multi-label extraction template" contentClassName="u-padding-top-none">
|
<Collapse isOpen={false} label="Multi-label extraction template" contentClassName="u-padding-top-none">
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<HorizontalGroup justify="space-between" style={{ marginBottom: '10px' }} align="flex-end">
|
<Stack justifyContent="space-between" alignItems="flex-end">
|
||||||
<Text type="secondary" size="small" className="u-padding-left-lg">
|
<Text type="secondary" size="small" className="u-padding-left-lg">
|
||||||
Allows for the extraction and modification of multiple labels from the alert payload using a single
|
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
|
template. Supports not only dynamic values but also dynamic keys. The Jinja template must result in
|
||||||
|
|
@ -172,7 +162,7 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
||||||
setShowTemplateEditor(true);
|
setShowTemplateEditor(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
value={alertGroupLabels.template}
|
value={alertGroupLabels.template}
|
||||||
height="200px"
|
height="200px"
|
||||||
|
|
@ -183,20 +173,20 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
||||||
setAlertGroupLabels({ ...alertGroupLabels, template: value });
|
setAlertGroupLabels({ ...alertGroupLabels, template: value });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|
||||||
<div className={cx('buttons')}>
|
<div className={cx('buttons')}>
|
||||||
<HorizontalGroup justify="flex-end">
|
<Stack justifyContent="flex-end">
|
||||||
<Button variant="secondary" onClick={onHide}>
|
<Button variant="secondary" onClick={onHide}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" onClick={handleSave}>
|
<Button variant="primary" onClick={handleSave}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
{customLabelIndexToShowTemplateEditor !== undefined && (
|
{customLabelIndexToShowTemplateEditor !== undefined && (
|
||||||
<IntegrationTemplate
|
<IntegrationTemplate
|
||||||
|
|
@ -297,7 +287,7 @@ const CustomLabels = (props: CustomLabelsProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Text>Dynamic & Static labels</Text>
|
<Text>Dynamic & Static labels</Text>
|
||||||
<Text type="secondary" size="small">
|
<Text type="secondary" size="small">
|
||||||
Dynamic: label values are extracted from the alert payload using Jinja. Keys remain static.
|
Dynamic: label values are extracted from the alert payload using Jinja. Keys remain static.
|
||||||
|
|
@ -376,6 +366,6 @@ const CustomLabels = (props: CustomLabelsProps) => {
|
||||||
Add label
|
Add label
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useCallback, useState, useEffect } from 'react';
|
import React, { useCallback, useState, useEffect } from 'react';
|
||||||
|
|
||||||
import { Button, HorizontalGroup, Drawer, VerticalGroup } from '@grafana/ui';
|
import { Button, Drawer, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
@ -165,13 +165,13 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
|
||||||
<Drawer
|
<Drawer
|
||||||
title={
|
title={
|
||||||
<div className={cx('title-container')}>
|
<div className={cx('title-container')}>
|
||||||
<HorizontalGroup justify="space-between" align="flex-start">
|
<Stack justifyContent="space-between" alignItems="flex-start">
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Text.Title level={3}>Edit {template.displayName} template</Text.Title>
|
<Text.Title level={3}>Edit {template.displayName} template</Text.Title>
|
||||||
{template.description && <Text type="secondary">{template.description}</Text>}
|
{template.description && <Text type="secondary">{template.description}</Text>}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
|
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||||
<Button variant="secondary" onClick={onHide}>
|
<Button variant="secondary" onClick={onHide}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|
@ -182,8 +182,8 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onClose={onHide}
|
onClose={onHide}
|
||||||
|
|
@ -231,13 +231,13 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
|
||||||
<>
|
<>
|
||||||
<div className={cx('template-block-codeeditor')}>
|
<div className={cx('template-block-codeeditor')}>
|
||||||
<div className={cx('template-editor-block-title')}>
|
<div className={cx('template-editor-block-title')}>
|
||||||
<HorizontalGroup justify="space-between" align="center" wrap>
|
<Stack justifyContent="space-between" alignItems="center" wrap="wrap">
|
||||||
<Text>Template editor</Text>
|
<Text>Template editor</Text>
|
||||||
|
|
||||||
<Button variant="secondary" fill="outline" onClick={onShowCheatSheet} icon="book" size="sm">
|
<Button variant="secondary" fill="outline" onClick={onShowCheatSheet} icon="book" size="sm">
|
||||||
Cheatsheet
|
Cheatsheet
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
<div className={cx('template-editor-block-content')}>
|
<div className={cx('template-editor-block-content')}>
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { Button, Icon, VerticalGroup, Field, Input } from '@grafana/ui';
|
import { Button, Icon, Stack, Field, Input } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
|
|
@ -10,6 +10,7 @@ import { PluginLink } from 'components/PluginLink/PluginLink';
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
import MSTeamsLogo from 'icons/MSTeamsLogo';
|
import MSTeamsLogo from 'icons/MSTeamsLogo';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
import { openNotification, openWarningNotification } from 'utils/utils';
|
import { openNotification, openWarningNotification } from 'utils/utils';
|
||||||
|
|
||||||
import styles from './MSTeamsInstructions.module.css';
|
import styles from './MSTeamsInstructions.module.css';
|
||||||
|
|
@ -40,36 +41,36 @@ export const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalGroup align="flex-start" spacing="lg">
|
<Stack direction="column" alignItems="flex-start" gap={StackSize.lg}>
|
||||||
{!personalSettings && <Text.Title level={2}>Connect MS Teams workspace</Text.Title>}
|
{!personalSettings && <Text.Title level={2}>Connect MS Teams workspace</Text.Title>}
|
||||||
{showInfoBox && (
|
{showInfoBox && (
|
||||||
<Block bordered withBackground className={cx('info-block')}>
|
<Block bordered withBackground className={cx('info-block')}>
|
||||||
<VerticalGroup align="center">
|
<Stack direction="column" alignItems="center">
|
||||||
<div style={{ width: '60px', marginTop: '24px' }}>
|
<div style={{ width: '60px', marginTop: '24px' }}>
|
||||||
<MSTeamsLogo />
|
<MSTeamsLogo />
|
||||||
</div>
|
</div>
|
||||||
<Text>You can manage alert groups in your Microsoft Teams workspace.</Text>
|
<Text>You can manage alert groups in your Microsoft Teams workspace.</Text>
|
||||||
<br />
|
<br />
|
||||||
{personalSettings ? (
|
{personalSettings ? (
|
||||||
<VerticalGroup align="center">
|
<Stack direction="column" alignItems="center">
|
||||||
<Text>This setup is for direct profile connection with bot. </Text>
|
<Text>This setup is for direct profile connection with bot. </Text>
|
||||||
<br />
|
<br />
|
||||||
<Text className={cx('infoblock-text')}>
|
<Text className={cx('infoblock-text')}>
|
||||||
To manage alert groups in Team channel, setup{' '}
|
To manage alert groups in Team channel, setup{' '}
|
||||||
<PluginLink query={{ page: 'chat-ops', tab: 'MSTeams' }}>Team ChatOps</PluginLink>
|
<PluginLink query={{ page: 'chat-ops', tab: 'MSTeams' }}>Team ChatOps</PluginLink>
|
||||||
</Text>
|
</Text>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<VerticalGroup align="center">
|
<Stack direction="column" alignItems="center">
|
||||||
<Text>This setup is for Team channel connection with bot. </Text>
|
<Text>This setup is for Team channel connection with bot. </Text>
|
||||||
<br />
|
<br />
|
||||||
<Text className={cx('infoblock-text')}>
|
<Text className={cx('infoblock-text')}>
|
||||||
To manage alert groups in Direct Messages and verify users who are allowed to operate with MS Teams,
|
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>
|
setup <PluginLink query={{ page: 'users', id: 'me' }}>personal MS Teams connection</PluginLink>
|
||||||
</Text>
|
</Text>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Block>
|
</Block>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -126,6 +127,6 @@ export const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props
|
||||||
<Button onClick={handleMSTeamsGetChannels}>Done</Button>
|
<Button onClick={handleMSTeamsGetChannels}>Done</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { Button, Drawer, Field, HorizontalGroup, Select, VerticalGroup, useStyles2 } from '@grafana/ui';
|
import { Button, Drawer, Field, Select, Stack, useStyles2 } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import Emoji from 'react-emoji-render';
|
import Emoji from 'react-emoji-render';
|
||||||
|
|
@ -74,7 +74,7 @@ export const MaintenanceForm = observer((props: MaintenanceFormProps) => {
|
||||||
return (
|
return (
|
||||||
<Drawer width="640px" scrollableContent title="Start Maintenance Mode" onClose={onHide} closeOnMaskClick={false}>
|
<Drawer width="640px" scrollableContent title="Start Maintenance Mode" onClose={onHide} closeOnMaskClick={false}>
|
||||||
<div className={cx('content')} data-testid="maintenance-mode-drawer">
|
<div className={cx('content')} data-testid="maintenance-mode-drawer">
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
Start maintenance mode when performing scheduled maintenance or updates on the infrastructure, which may
|
Start maintenance mode when performing scheduled maintenance or updates on the infrastructure, which may
|
||||||
trigger false alarms.
|
trigger false alarms.
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
|
|
@ -182,7 +182,7 @@ export const MaintenanceForm = observer((props: MaintenanceFormProps) => {
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<HorizontalGroup justify="flex-end">
|
<Stack justifyContent="flex-end">
|
||||||
<Button variant="secondary" onClick={onHide}>
|
<Button variant="secondary" onClick={onHide}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -191,10 +191,10 @@ export const MaintenanceForm = observer((props: MaintenanceFormProps) => {
|
||||||
Start
|
Start
|
||||||
</Button>
|
</Button>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||||
|
|
||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { MemoryRouter } from 'react-router-dom-v5-compat';
|
|
||||||
|
|
||||||
import { UserHelper } from 'models/user/user.helpers';
|
import { UserHelper } from 'models/user/user.helpers';
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
|
|
@ -64,8 +63,7 @@ describe('MobileAppConnection', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it shows a loading message if it is currently fetching the QR code', async () => {
|
test('it shows a loading message if it is currently fetching the QR code', async () => {
|
||||||
const component = render(<MobileAppConnection userPk={USER_PK} />);
|
render(<MobileAppConnection userPk={USER_PK} />);
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
||||||
|
|
@ -75,20 +73,17 @@ describe('MobileAppConnection', () => {
|
||||||
|
|
||||||
test('it shows an error message if there was an error fetching the QR code', async () => {
|
test('it shows an error message if there was an error fetching the QR code', async () => {
|
||||||
UserHelper.fetchBackendConfirmationCode = jest.fn().mockRejectedValueOnce('dfd');
|
UserHelper.fetchBackendConfirmationCode = jest.fn().mockRejectedValueOnce('dfd');
|
||||||
const component = render(<MobileAppConnection userPk={USER_PK} />);
|
render(<MobileAppConnection userPk={USER_PK} />);
|
||||||
await screen.findByText(/.*error fetching your QR code.*/);
|
await screen.findByText(/.*error fetching your QR code.*/);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
|
|
||||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
||||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledWith(USER_PK, BACKEND);
|
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledWith(USER_PK, BACKEND);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("it shows a QR code if the app isn't already connected", async () => {
|
test("it shows a QR code if the app isn't already connected", async () => {
|
||||||
const component = render(<MobileAppConnection userPk={USER_PK} />);
|
render(<MobileAppConnection userPk={USER_PK} />);
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
||||||
|
|
@ -113,8 +108,6 @@ describe('MobileAppConnection', () => {
|
||||||
// click the confirm button within the modal, which actually triggers the callback
|
// click the confirm button within the modal, which actually triggers the callback
|
||||||
await userEvent.click(screen.getByText('Remove'));
|
await userEvent.click(screen.getByText('Remove'));
|
||||||
|
|
||||||
// expect(component.container).toMatchSnapshot();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
||||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledWith(USER_PK, BACKEND);
|
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledWith(USER_PK, BACKEND);
|
||||||
|
|
@ -132,7 +125,7 @@ describe('MobileAppConnection', () => {
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
const component = render(<MobileAppConnection userPk={USER_PK} />);
|
render(<MobileAppConnection userPk={USER_PK} />);
|
||||||
const button = await screen.findByRole('button');
|
const button = await screen.findByRole('button');
|
||||||
|
|
||||||
// click the disconnect button, which opens the modal
|
// click the disconnect button, which opens the modal
|
||||||
|
|
@ -143,8 +136,6 @@ describe('MobileAppConnection', () => {
|
||||||
// wait for loading state
|
// wait for loading state
|
||||||
await screen.findByText(/.*Loading.*/);
|
await screen.findByText(/.*Loading.*/);
|
||||||
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
||||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledWith(USER_PK, BACKEND);
|
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledWith(USER_PK, BACKEND);
|
||||||
|
|
@ -162,7 +153,7 @@ describe('MobileAppConnection', () => {
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
const component = render(<MobileAppConnection userPk={USER_PK} />);
|
render(<MobileAppConnection userPk={USER_PK} />);
|
||||||
const button = await screen.findByTestId('test__disconnect');
|
const button = await screen.findByTestId('test__disconnect');
|
||||||
|
|
||||||
// click the disconnect button, which opens the modal
|
// click the disconnect button, which opens the modal
|
||||||
|
|
@ -172,8 +163,6 @@ describe('MobileAppConnection', () => {
|
||||||
|
|
||||||
await screen.findByText(/.*error disconnecting your mobile app.*/);
|
await screen.findByText(/.*error disconnecting your mobile app.*/);
|
||||||
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(0);
|
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
|
@ -223,16 +212,4 @@ describe('MobileAppConnection', () => {
|
||||||
{ timeout: 6000 }
|
{ timeout: 6000 }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it shows a warning when cloud is not connected', async () => {
|
|
||||||
mockRootStore({}, true, false);
|
|
||||||
|
|
||||||
// Using MemoryRouter to avoid "Invariant failed: You should not use <Link> outside a <Router>"
|
|
||||||
const component = render(
|
|
||||||
<MemoryRouter>
|
|
||||||
<MobileAppConnection userPk={USER_PK} />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Button, HorizontalGroup, Icon, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
import { Button, Icon, LoadingPlaceholder, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import { AppFeature } from 'state/features';
|
import { AppFeature } from 'state/features';
|
||||||
import { RootStore, rootStore as store } from 'state/rootStore';
|
import { RootStore, rootStore as store } from 'state/rootStore';
|
||||||
import { UserActions } from 'utils/authorization/authorization';
|
import { UserActions } from 'utils/authorization/authorization';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
import { useInitializePlugin } from 'utils/hooks';
|
import { useInitializePlugin } from 'utils/hooks';
|
||||||
import { isMobile, openErrorNotification, openNotification, openWarningNotification } from 'utils/utils';
|
import { isMobile, openErrorNotification, openNotification, openWarningNotification } from 'utils/utils';
|
||||||
|
|
||||||
|
|
@ -155,7 +156,7 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
|
||||||
content = <Text type="primary">{errorFetchingQRCode || errorDisconnectingMobileApp}</Text>;
|
content = <Text type="primary">{errorFetchingQRCode || errorDisconnectingMobileApp}</Text>;
|
||||||
} else if (mobileAppIsCurrentlyConnected) {
|
} else if (mobileAppIsCurrentlyConnected) {
|
||||||
content = (
|
content = (
|
||||||
<VerticalGroup spacing="lg">
|
<Stack direction="column" gap={StackSize.lg}>
|
||||||
<Text strong type="primary">
|
<Text strong type="primary">
|
||||||
App connected <Icon name="check-circle" size="md" className={cx('icon')} />
|
App connected <Icon name="check-circle" size="md" className={cx('icon')} />
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -167,11 +168,11 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
|
||||||
<img src={qrCodeImage} className={cx('disconnect__qrCode')} />
|
<img src={qrCodeImage} className={cx('disconnect__qrCode')} />
|
||||||
<DisconnectButton onClick={disconnectMobileApp} />
|
<DisconnectButton onClick={disconnectMobileApp} />
|
||||||
</div>
|
</div>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
} else if (QRCodeValue) {
|
} else if (QRCodeValue) {
|
||||||
content = (
|
content = (
|
||||||
<VerticalGroup spacing="lg">
|
<Stack direction="column" gap={StackSize.lg}>
|
||||||
<Text type="primary" strong>
|
<Text type="primary" strong>
|
||||||
Sign in via QR Code
|
Sign in via QR Code
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -191,14 +192,14 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
|
||||||
</a>
|
</a>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3>Mobile App Connection</h3>
|
<h3>Mobile App Connection</h3>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<div className={cx('container')}>
|
<div className={cx('container')}>
|
||||||
{QRCodeDataParsed && isMobile && (
|
{QRCodeDataParsed && isMobile && (
|
||||||
<Block shadowed bordered withBackground className={cx('container__box')}>
|
<Block shadowed bordered withBackground className={cx('container__box')}>
|
||||||
|
|
@ -214,7 +215,7 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
{mobileAppIsCurrentlyConnected && isCurrentUser && !disconnectingMobileApp && (
|
{mobileAppIsCurrentlyConnected && isCurrentUser && !disconnectingMobileApp && (
|
||||||
<div className={cx('notification-buttons')}>
|
<div className={cx('notification-buttons')}>
|
||||||
<HorizontalGroup spacing={'md'} justify={'flex-end'}>
|
<Stack gap={StackSize.md} justifyContent={'flex-end'}>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => onSendTestNotification()}
|
onClick={() => onSendTestNotification()}
|
||||||
|
|
@ -229,17 +230,17 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
|
||||||
>
|
>
|
||||||
Send Test Push Important
|
Send Test Push Important
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
function renderConnectToCloud() {
|
function renderConnectToCloud() {
|
||||||
return (
|
return (
|
||||||
<WithPermissionControlDisplay userAction={UserActions.UserSettingsWrite}>
|
<WithPermissionControlDisplay userAction={UserActions.UserSettingsWrite}>
|
||||||
<VerticalGroup spacing="lg">
|
<Stack direction="column" gap={StackSize.lg}>
|
||||||
<Text type="secondary">Please connect Grafana Cloud OnCall to use the mobile app</Text>
|
<Text type="secondary">Please connect Grafana Cloud OnCall to use the mobile app</Text>
|
||||||
<WithPermissionControlDisplay
|
<WithPermissionControlDisplay
|
||||||
userAction={UserActions.OtherSettingsWrite}
|
userAction={UserActions.OtherSettingsWrite}
|
||||||
|
|
@ -252,7 +253,7 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
|
||||||
</Button>
|
</Button>
|
||||||
</PluginLink>
|
</PluginLink>
|
||||||
</WithPermissionControlDisplay>
|
</WithPermissionControlDisplay>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</WithPermissionControlDisplay>
|
</WithPermissionControlDisplay>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,635 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`MobileAppConnection it shows a QR code if the app isn't already connected 1`] = `
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
Mobile App Connection
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--shadowed css-1x53p5e--withBackGround container__box"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1yjvs5a"
|
|
||||||
>
|
|
||||||
Loading...
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="css-1baulvz"
|
|
||||||
data-testid="Spinner"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--shadowed css-1x53p5e--withBackGround container__box"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-77ouhj--strong css-66w5es"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
The Grafana OnCall app is available on both the App Store and Google Play Store.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
|
|
||||||
rel="noreferrer"
|
|
||||||
style="width: 100%;"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--fullWidth css-1x53p5e--withBackGround css-1x53p5e--hover icon-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Apple"
|
|
||||||
class="icon"
|
|
||||||
src="[object Object]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline icon-text css-66w5es"
|
|
||||||
>
|
|
||||||
iOS
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
|
|
||||||
rel="noreferrer"
|
|
||||||
style="width: 100%;"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--fullWidth css-1x53p5e--hover icon-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Play Store"
|
|
||||||
class="icon"
|
|
||||||
src="[object Object]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline icon-text css-66w5es"
|
|
||||||
>
|
|
||||||
Android
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`MobileAppConnection it shows a loading message if it is currently disconnecting 1`] = `
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
Mobile App Connection
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--shadowed css-1x53p5e--withBackGround container__box"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1yjvs5a"
|
|
||||||
>
|
|
||||||
Loading...
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="css-1baulvz"
|
|
||||||
data-testid="Spinner"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--shadowed css-1x53p5e--withBackGround container__box"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-77ouhj--strong css-66w5es"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
The Grafana OnCall app is available on both the App Store and Google Play Store.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
|
|
||||||
rel="noreferrer"
|
|
||||||
style="width: 100%;"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--fullWidth css-1x53p5e--withBackGround css-1x53p5e--hover icon-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Apple"
|
|
||||||
class="icon"
|
|
||||||
src="[object Object]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline icon-text css-66w5es"
|
|
||||||
>
|
|
||||||
iOS
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
|
|
||||||
rel="noreferrer"
|
|
||||||
style="width: 100%;"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--fullWidth css-1x53p5e--hover icon-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Play Store"
|
|
||||||
class="icon"
|
|
||||||
src="[object Object]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline icon-text css-66w5es"
|
|
||||||
>
|
|
||||||
Android
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`MobileAppConnection it shows a loading message if it is currently fetching the QR code 1`] = `
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
Mobile App Connection
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--shadowed css-1x53p5e--withBackGround container__box"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1yjvs5a"
|
|
||||||
>
|
|
||||||
Loading...
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="css-1baulvz"
|
|
||||||
data-testid="Spinner"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--shadowed css-1x53p5e--withBackGround container__box"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-77ouhj--strong css-66w5es"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
The Grafana OnCall app is available on both the App Store and Google Play Store.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
|
|
||||||
rel="noreferrer"
|
|
||||||
style="width: 100%;"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--fullWidth css-1x53p5e--withBackGround css-1x53p5e--hover icon-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Apple"
|
|
||||||
class="icon"
|
|
||||||
src="[object Object]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline icon-text css-66w5es"
|
|
||||||
>
|
|
||||||
iOS
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
|
|
||||||
rel="noreferrer"
|
|
||||||
style="width: 100%;"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--fullWidth css-1x53p5e--hover icon-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Play Store"
|
|
||||||
class="icon"
|
|
||||||
src="[object Object]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline icon-text css-66w5es"
|
|
||||||
>
|
|
||||||
Android
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`MobileAppConnection it shows a warning when cloud is not connected 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--secondary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
Please connect Grafana Cloud OnCall to use the mobile app
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="css-1xyvdh3"
|
|
||||||
href="/a/grafana-oncall-app/cloud"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-disabled="false"
|
|
||||||
class="css-8b29hm-button"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1riaxdn"
|
|
||||||
>
|
|
||||||
Connect Grafana Cloud OnCall
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`MobileAppConnection it shows an error message if there was an error disconnecting the mobile app 1`] = `
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
Mobile App Connection
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--shadowed css-1x53p5e--withBackGround container__box"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
There was an error disconnecting your mobile app. Please try again.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--shadowed css-1x53p5e--withBackGround container__box"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-77ouhj--strong css-66w5es"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
The Grafana OnCall app is available on both the App Store and Google Play Store.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
|
|
||||||
rel="noreferrer"
|
|
||||||
style="width: 100%;"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--fullWidth css-1x53p5e--withBackGround css-1x53p5e--hover icon-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Apple"
|
|
||||||
class="icon"
|
|
||||||
src="[object Object]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline icon-text css-66w5es"
|
|
||||||
>
|
|
||||||
iOS
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
|
|
||||||
rel="noreferrer"
|
|
||||||
style="width: 100%;"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--fullWidth css-1x53p5e--hover icon-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Play Store"
|
|
||||||
class="icon"
|
|
||||||
src="[object Object]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline icon-text css-66w5es"
|
|
||||||
>
|
|
||||||
Android
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`MobileAppConnection it shows an error message if there was an error fetching the QR code 1`] = `
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
Mobile App Connection
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="container"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--shadowed css-1x53p5e--withBackGround container__box"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
There was an error fetching your QR code. Please try again.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--shadowed css-1x53p5e--withBackGround container__box"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-77ouhj--strong css-66w5es"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
The Grafana OnCall app is available on both the App Store and Google Play Store.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
|
|
||||||
rel="noreferrer"
|
|
||||||
style="width: 100%;"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--fullWidth css-1x53p5e--withBackGround css-1x53p5e--hover icon-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Apple"
|
|
||||||
class="icon"
|
|
||||||
src="[object Object]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline icon-text css-66w5es"
|
|
||||||
>
|
|
||||||
iOS
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
|
|
||||||
rel="noreferrer"
|
|
||||||
style="width: 100%;"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--fullWidth css-1x53p5e--hover icon-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Play Store"
|
|
||||||
class="icon"
|
|
||||||
src="[object Object]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline icon-text css-66w5es"
|
|
||||||
>
|
|
||||||
Android
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
@ -6,11 +6,6 @@ import userEvent from '@testing-library/user-event';
|
||||||
import { DisconnectButton } from './DisconnectButton';
|
import { DisconnectButton } from './DisconnectButton';
|
||||||
|
|
||||||
describe('DisconnectButton', () => {
|
describe('DisconnectButton', () => {
|
||||||
test('it renders properly', () => {
|
|
||||||
const component = render(<DisconnectButton onClick={() => {}} />);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('It calls the onClick handler when clicked', async () => {
|
test('It calls the onClick handler when clicked', async () => {
|
||||||
const mockedOnClick = jest.fn();
|
const mockedOnClick = jest.fn();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`DisconnectButton it renders properly 1`] = `
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
aria-disabled="false"
|
|
||||||
class="css-ttl745-button disconnect-button"
|
|
||||||
data-testid="test__disconnect"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1riaxdn"
|
|
||||||
>
|
|
||||||
Disconnect
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
|
|
||||||
import { DownloadIcons } from './DownloadIcons';
|
|
||||||
|
|
||||||
describe('DownloadIcons', () => {
|
|
||||||
test('it renders properly', () => {
|
|
||||||
const component = render(<DownloadIcons />);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,24 +1,25 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { VerticalGroup } from '@grafana/ui';
|
import { Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
|
|
||||||
import AppleLogoSVG from 'assets/img/apple-logo.svg';
|
import AppleLogoSVG from 'assets/img/apple-logo.svg';
|
||||||
import PlayStoreLogoSVG from 'assets/img/play-store-logo.svg';
|
import PlayStoreLogoSVG from 'assets/img/play-store-logo.svg';
|
||||||
import { Block } from 'components/GBlock/Block';
|
import { Block } from 'components/GBlock/Block';
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import styles from './DownloadIcons.module.scss';
|
import styles from './DownloadIcons.module.scss';
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
const cx = cn.bind(styles);
|
||||||
|
|
||||||
export const DownloadIcons: FC = () => (
|
export const DownloadIcons: FC = () => (
|
||||||
<VerticalGroup spacing="lg">
|
<Stack direction="column" gap={StackSize.lg}>
|
||||||
<Text type="primary" strong>
|
<Text type="primary" strong>
|
||||||
Download
|
Download
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="primary">The Grafana OnCall app is available on both the App Store and Google Play Store.</Text>
|
<Text type="primary">The Grafana OnCall app is available on both the App Store and Google Play Store.</Text>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<a
|
<a
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
|
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
|
||||||
|
|
@ -45,6 +46,6 @@ export const DownloadIcons: FC = () => (
|
||||||
</Text>
|
</Text>
|
||||||
</Block>
|
</Block>
|
||||||
</a>
|
</a>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`DownloadIcons it renders properly 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-77ouhj--strong css-66w5es"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
The Grafana OnCall app is available on both the App Store and Google Play Store.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
|
|
||||||
rel="noreferrer"
|
|
||||||
style="width: 100%;"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--fullWidth css-1x53p5e--withBackGround css-1x53p5e--hover icon-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Apple"
|
|
||||||
class="icon"
|
|
||||||
src="[object Object]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline icon-text css-66w5es"
|
|
||||||
>
|
|
||||||
iOS
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-gxt817-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
|
|
||||||
rel="noreferrer"
|
|
||||||
style="width: 100%;"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered css-1x53p5e--fullWidth css-1x53p5e--hover icon-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt="Play Store"
|
|
||||||
class="icon"
|
|
||||||
src="[object Object]"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline icon-text css-66w5es"
|
|
||||||
>
|
|
||||||
Android
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
|
|
||||||
import { LinkLoginButton } from './LinkLoginButton';
|
|
||||||
|
|
||||||
describe('LinkLoginButton', () => {
|
|
||||||
test('it renders properly', () => {
|
|
||||||
const component = render(<LinkLoginButton baseUrl="http://test.url" token="test1213" />);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { Button, VerticalGroup } from '@grafana/ui';
|
import { Button, Stack } from '@grafana/ui';
|
||||||
|
|
||||||
import { Text } from 'components/Text/Text';
|
import { Text } from 'components/Text/Text';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
|
@ -14,7 +15,7 @@ export const LinkLoginButton: FC<Props> = (props: Props) => {
|
||||||
const mobileDeepLink = `grafana://mobile/login/link-login?oncall_api_url=${baseUrl}&token=${token}`;
|
const mobileDeepLink = `grafana://mobile/login/link-login?oncall_api_url=${baseUrl}&token=${token}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalGroup spacing="lg">
|
<Stack direction="column" gap={StackSize.lg}>
|
||||||
<Text type="primary" strong>
|
<Text type="primary" strong>
|
||||||
Sign in via deeplink
|
Sign in via deeplink
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -27,6 +28,6 @@ export const LinkLoginButton: FC<Props> = (props: Props) => {
|
||||||
>
|
>
|
||||||
Connect Mobile App
|
Connect Mobile App
|
||||||
</Button>
|
</Button>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`LinkLoginButton it renders properly 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-gjl87o-vertical-group"
|
|
||||||
style="width: 100%; height: 100%;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-77ouhj--strong css-66w5es"
|
|
||||||
>
|
|
||||||
Sign in via deeplink
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-77ouhj--primary css-77ouhj--medium css-1rchs6o--inline css-66w5es"
|
|
||||||
>
|
|
||||||
Make sure to have the app installed
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="css-12oo3x0-layoutChildrenWrapper"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-disabled="false"
|
|
||||||
class="css-td06pi-button"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="css-1riaxdn"
|
|
||||||
>
|
|
||||||
Connect Mobile App
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
|
|
||||||
import { QRCode } from './QRCode';
|
|
||||||
|
|
||||||
describe('QRCode', () => {
|
|
||||||
test('it renders properly', () => {
|
|
||||||
const component = render(<QRCode value="helloooo" />);
|
|
||||||
expect(component.container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`QRCode it renders properly 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="css-1x53p5e css-1x53p5e--bordered"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
height="256"
|
|
||||||
viewBox="0 0 21 21"
|
|
||||||
width="256"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M0,0 h21v21H0z"
|
|
||||||
fill="#FFFFFF"
|
|
||||||
shape-rendering="crispEdges"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M0 0h7v1H0zM8 0h1v1H8zM12 0h1v1H12zM14,0 h7v1H14zM0 1h1v1H0zM6 1h1v1H6zM9 1h1v1H9zM11 1h2v1H11zM14 1h1v1H14zM20,1 h1v1H20zM0 2h1v1H0zM2 2h3v1H2zM6 2h1v1H6zM9 2h3v1H9zM14 2h1v1H14zM16 2h3v1H16zM20,2 h1v1H20zM0 3h1v1H0zM2 3h3v1H2zM6 3h1v1H6zM10 3h1v1H10zM14 3h1v1H14zM16 3h3v1H16zM20,3 h1v1H20zM0 4h1v1H0zM2 4h3v1H2zM6 4h1v1H6zM8 4h3v1H8zM12 4h1v1H12zM14 4h1v1H14zM16 4h3v1H16zM20,4 h1v1H20zM0 5h1v1H0zM6 5h1v1H6zM8 5h3v1H8zM14 5h1v1H14zM20,5 h1v1H20zM0 6h7v1H0zM8 6h1v1H8zM10 6h1v1H10zM12 6h1v1H12zM14,6 h7v1H14zM1 8h7v1H1zM11 8h2v1H11zM15 8h2v1H15zM20,8 h1v1H20zM0 9h2v1H0zM3 9h1v1H3zM8 9h1v1H8zM12 9h7v1H12zM20,9 h1v1H20zM3 10h1v1H3zM5 10h2v1H5zM9 10h1v1H9zM11 10h1v1H11zM13 10h2v1H13zM17 10h3v1H17zM0 11h1v1H0zM4 11h2v1H4zM7 11h1v1H7zM12 11h1v1H12zM16 11h3v1H16zM1 12h2v1H1zM4 12h1v1H4zM6 12h4v1H6zM11 12h1v1H11zM16 12h1v1H16zM20,12 h1v1H20zM8 13h3v1H8zM12 13h6v1H12zM20,13 h1v1H20zM0 14h7v1H0zM8 14h1v1H8zM12 14h1v1H12zM14 14h1v1H14zM18 14h2v1H18zM0 15h1v1H0zM6 15h1v1H6zM8 15h2v1H8zM13 15h2v1H13zM16 15h3v1H16zM0 16h1v1H0zM2 16h3v1H2zM6 16h1v1H6zM8 16h3v1H8zM16 16h1v1H16zM20,16 h1v1H20zM0 17h1v1H0zM2 17h3v1H2zM6 17h1v1H6zM8 17h2v1H8zM12 17h1v1H12zM15 17h3v1H15zM0 18h1v1H0zM2 18h3v1H2zM6 18h1v1H6zM8 18h2v1H8zM11 18h1v1H11zM13 18h1v1H13zM18 18h1v1H18zM0 19h1v1H0zM6 19h1v1H6zM8 19h1v1H8zM16 19h3v1H16zM0 20h7v1H0zM11 20h1v1H11zM13 20h1v1H13zM16 20h1v1H16zM19 20h1v1H19z"
|
|
||||||
fill="#000000"
|
|
||||||
shape-rendering="crispEdges"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
@ -1,16 +1,6 @@
|
||||||
import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import { Button, ConfirmModal, ConfirmModalProps, Drawer, Input, Tab, TabsBar, Stack } from '@grafana/ui';
|
||||||
Button,
|
|
||||||
ConfirmModal,
|
|
||||||
ConfirmModalProps,
|
|
||||||
Drawer,
|
|
||||||
HorizontalGroup,
|
|
||||||
Input,
|
|
||||||
Tab,
|
|
||||||
TabsBar,
|
|
||||||
VerticalGroup,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { FormProvider, useForm, useFormContext } from 'react-hook-form';
|
import { FormProvider, useForm, useFormContext } from 'react-hook-form';
|
||||||
|
|
@ -212,7 +202,7 @@ const Presets = (props: PresetsProps) => {
|
||||||
return (
|
return (
|
||||||
<Drawer scrollableContent title="New Outgoing Webhook" onClose={onHide} closeOnMaskClick={false} width="640px">
|
<Drawer scrollableContent title="New Outgoing Webhook" onClose={onHide} closeOnMaskClick={false} width="640px">
|
||||||
<div className={cx('content')}>
|
<div className={cx('content')}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
Outgoing webhooks can send alert data to other systems. They can be triggered by various conditions and can
|
Outgoing webhooks can send alert data to other systems. They can be triggered by various conditions and can
|
||||||
use templates to transform data to fit the recipient system. Presets listed below provide a starting point
|
use templates to transform data to fit the recipient system. Presets listed below provide a starting point
|
||||||
|
|
@ -231,7 +221,7 @@ const Presets = (props: PresetsProps) => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<WebhookPresetBlocks presets={presets} onBlockClick={onSelect} />
|
<WebhookPresetBlocks presets={presets} onBlockClick={onSelect} />
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
|
@ -265,7 +255,7 @@ const NewWebhook = (props: NewWebhookProps) => {
|
||||||
onTemplateEditClick={onTemplateEditClick}
|
onTemplateEditClick={onTemplateEditClick}
|
||||||
/>
|
/>
|
||||||
<div className={cx('buttons')}>
|
<div className={cx('buttons')}>
|
||||||
<HorizontalGroup justify="flex-end">
|
<Stack justifyContent="flex-end">
|
||||||
{action === WebhookFormActionType.NEW ? (
|
{action === WebhookFormActionType.NEW ? (
|
||||||
<Button variant="secondary" onClick={onBack}>
|
<Button variant="secondary" onClick={onBack}>
|
||||||
Back
|
Back
|
||||||
|
|
@ -280,7 +270,7 @@ const NewWebhook = (props: NewWebhookProps) => {
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -400,7 +390,7 @@ const WebhookTabsContent: React.FC<WebhookTabsProps> = observer(
|
||||||
onTemplateEditClick={onTemplateEditClick}
|
onTemplateEditClick={onTemplateEditClick}
|
||||||
/>
|
/>
|
||||||
<div className={cx('buttons')}>
|
<div className={cx('buttons')}>
|
||||||
<HorizontalGroup justify={'flex-end'}>
|
<Stack justifyContent={'flex-end'}>
|
||||||
<Button variant="secondary" onClick={onHide}>
|
<Button variant="secondary" onClick={onHide}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -428,7 +418,7 @@ const WebhookTabsContent: React.FC<WebhookTabsProps> = observer(
|
||||||
{action === WebhookFormActionType.NEW ? 'Create' : 'Update'}
|
{action === WebhookFormActionType.NEW ? 'Create' : 'Update'}
|
||||||
</Button>
|
</Button>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ export const OutgoingWebhookFormFields: React.FC<OutgoingWebhookFormFieldsProps>
|
||||||
placeholder="Choose (Optional)"
|
placeholder="Choose (Optional)"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
|
dataTestId="team-selector"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
|
|
@ -128,6 +129,7 @@ export const OutgoingWebhookFormFields: React.FC<OutgoingWebhookFormFieldsProps>
|
||||||
error={errors.trigger_type?.message}
|
error={errors.trigger_type?.message}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
data-testid="triggerType-selector"
|
||||||
placeholder="Choose (Required)"
|
placeholder="Choose (Required)"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { EmptySearchResult, HorizontalGroup, VerticalGroup } from '@grafana/ui';
|
import { EmptySearchResult, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { Text } from 'components/Text/Text';
|
||||||
import { getWebhookPresetIcons } from 'containers/OutgoingWebhookForm/WebhookPresetIcons.config';
|
import { getWebhookPresetIcons } from 'containers/OutgoingWebhookForm/WebhookPresetIcons.config';
|
||||||
import { OutgoingWebhookPreset } from 'models/outgoing_webhook/outgoing_webhook.types';
|
import { OutgoingWebhookPreset } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
|
|
||||||
import styles from 'containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css';
|
import styles from 'containers/OutgoingWebhookForm/OutgoingWebhookForm.module.css';
|
||||||
|
|
||||||
|
|
@ -38,16 +39,16 @@ export const WebhookPresetBlocks: React.FC<{
|
||||||
<Block bordered hover shadowed onClick={() => onBlockClick(preset)} key={preset.id} className={cx('card')}>
|
<Block bordered hover shadowed onClick={() => onBlockClick(preset)} key={preset.id} className={cx('card')}>
|
||||||
<div className={cx('card-bg')}>{logo}</div>
|
<div className={cx('card-bg')}>{logo}</div>
|
||||||
<div className={cx('title')}>
|
<div className={cx('title')}>
|
||||||
<VerticalGroup spacing="xs">
|
<Stack direction="column" gap={StackSize.xs}>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Text strong data-testid="webhook-preset-display-name">
|
<Text strong data-testid="webhook-preset-display-name">
|
||||||
{preset.name}
|
{preset.name}
|
||||||
</Text>
|
</Text>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<Text type="secondary" size="small">
|
<Text type="secondary" size="small">
|
||||||
{preset.description}
|
{preset.description}
|
||||||
</Text>
|
</Text>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { HorizontalGroup, Button } from '@grafana/ui';
|
import { Button, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
|
@ -30,11 +30,11 @@ export const OutgoingWebhookStatus = observer(({ id, closeDrawer }: OutgoingWebh
|
||||||
<div className={cx('content')}>
|
<div className={cx('content')}>
|
||||||
<WebhookLastEventDetails webhook={webhook} sourceCodeRootClassName={cx('sourceCodeRoot')} />
|
<WebhookLastEventDetails webhook={webhook} sourceCodeRootClassName={cx('sourceCodeRoot')} />
|
||||||
<div className={commonStyles.bottomDrawerButtons}>
|
<div className={commonStyles.bottomDrawerButtons}>
|
||||||
<HorizontalGroup justify="flex-end">
|
<Stack justifyContent="flex-end">
|
||||||
<Button variant="secondary" onClick={closeDrawer}>
|
<Button variant="secondary" onClick={closeDrawer}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import { Button, HorizontalGroup, Icon, LoadingPlaceholder, Tooltip } from '@grafana/ui';
|
import { Button, Icon, LoadingPlaceholder, Stack, Tooltip } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import { get } from 'lodash-es';
|
import { get } from 'lodash-es';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
@ -65,7 +65,7 @@ export const PersonalNotificationSettings = observer((props: PersonalNotificatio
|
||||||
const allNotificationPolicies = userStore.notificationPolicies[userPk];
|
const allNotificationPolicies = userStore.notificationPolicies[userPk];
|
||||||
const title = (
|
const title = (
|
||||||
<Text.Title level={5}>
|
<Text.Title level={5}>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
{isImportant ? 'Important Notifications' : 'Default Notifications'}
|
{isImportant ? 'Important Notifications' : 'Default Notifications'}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
placement="top"
|
placement="top"
|
||||||
|
|
@ -77,7 +77,7 @@ export const PersonalNotificationSettings = observer((props: PersonalNotificatio
|
||||||
>
|
>
|
||||||
<Icon name="info-circle" size="md"></Icon>
|
<Icon name="info-circle" size="md"></Icon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</Text.Title>
|
</Text.Title>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2, PluginConfigPageProps, PluginMeta } from '@grafana/data';
|
import { GrafanaTheme2, PluginConfigPageProps, PluginMeta } from '@grafana/data';
|
||||||
import { Alert, Field, HorizontalGroup, Input, LoadingPlaceholder, useStyles2, VerticalGroup } from '@grafana/ui';
|
import { Alert, Field, Input, LoadingPlaceholder, useStyles2, Stack } from '@grafana/ui';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||||
|
|
@ -41,12 +41,12 @@ export const PluginConfigPage = observer((props: PluginConfigPageProps<PluginMet
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Text.Title level={3} className="u-margin-bottom-md">
|
<Text.Title level={3} className="u-margin-bottom-md">
|
||||||
Configure Grafana OnCall
|
Configure Grafana OnCall
|
||||||
</Text.Title>
|
</Text.Title>
|
||||||
{getIsRunningOpenSourceVersion() ? <OSSPluginConfigPage {...props} /> : <CloudPluginConfigPage {...props} />}
|
{getIsRunningOpenSourceVersion() ? <OSSPluginConfigPage {...props} /> : <CloudPluginConfigPage {...props} />}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ const CloudPluginConfigPage = observer(
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Text type="secondary" className={styles.secondaryTitle}>
|
<Text type="secondary" className={styles.secondaryTitle}>
|
||||||
This is a cloud-managed configuration.
|
This is a cloud-managed configuration.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -67,7 +67,7 @@ const CloudPluginConfigPage = observer(
|
||||||
shouldRender={!isPluginConnected}
|
shouldRender={!isPluginConnected}
|
||||||
render={() => <Button onClick={() => window.open(REQUEST_HELP_URL, '_blank')}>Request help</Button>}
|
render={() => <Button onClick={() => window.open(REQUEST_HELP_URL, '_blank')}>Request help</Button>}
|
||||||
/>
|
/>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -135,7 +135,7 @@ const OSSPluginConfigPage = observer(
|
||||||
<Text type="link">Read more</Text>
|
<Text type="link">Read more</Text>
|
||||||
</a>
|
</a>
|
||||||
</Text>
|
</Text>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={recreateServiceAccountAndRecheckPluginStatus}
|
onClick={recreateServiceAccountAndRecheckPluginStatus}
|
||||||
|
|
@ -147,7 +147,7 @@ const OSSPluginConfigPage = observer(
|
||||||
shouldRender={isRecreatingServiceAccount}
|
shouldRender={isRecreatingServiceAccount}
|
||||||
render={() => <LoadingPlaceholder text="" className={styles.spinner} />}
|
render={() => <LoadingPlaceholder text="" className={styles.spinner} />}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -178,7 +178,7 @@ const OSSPluginConfigPage = observer(
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
{isPluginConnected && (
|
{isPluginConnected && (
|
||||||
<Button onClick={() => navigate(`${PLUGIN_ROOT}/${DEFAULT_PAGE}`)}>Open Grafana OnCall</Button>
|
<Button onClick={() => navigate(`${PLUGIN_ROOT}/${DEFAULT_PAGE}`)}>Open Grafana OnCall</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -194,7 +194,7 @@ const OSSPluginConfigPage = observer(
|
||||||
shouldRender={isReinitializating}
|
shouldRender={isReinitializating}
|
||||||
render={() => <LoadingPlaceholder text="" className={styles.spinner} />}
|
render={() => <LoadingPlaceholder text="" className={styles.spinner} />}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
import { Button, HorizontalGroup, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
import { Button, LoadingPlaceholder, Stack } from '@grafana/ui';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
@ -19,9 +19,9 @@ export const PluginInitializer: FC<PluginInitializerProps> = observer(({ childre
|
||||||
|
|
||||||
if (isCheckingConnectionStatus) {
|
if (isCheckingConnectionStatus) {
|
||||||
return (
|
return (
|
||||||
<VerticalGroup justify="center" height="100%" align="center">
|
<Stack direction="column" justifyContent="center" height="100%" alignItems="center">
|
||||||
<LoadingPlaceholder text="Loading..." />
|
<LoadingPlaceholder text="Loading..." />
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|
@ -57,13 +57,13 @@ const PluginNotConnectedFullPageError = observer(() => {
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Button variant="secondary" onClick={() => window.location.reload()}>
|
<Button variant="secondary" onClick={() => window.location.reload()}>
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
{!isOpenSource && <Button onClick={() => window.open(REQUEST_HELP_URL, '_blank')}>Request help</Button>}
|
{!isOpenSource && <Button onClick={() => window.open(REQUEST_HELP_URL, '_blank')}>Request help</Button>}
|
||||||
{isOpenSource && isCurrentUserAdmin && <Button onClick={() => push(PLUGIN_CONFIG)}>Open configuration</Button>}
|
{isOpenSource && isCurrentUserAdmin && <Button onClick={() => push(PLUGIN_CONFIG)}>Open configuration</Button>}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</FullPageError>
|
</FullPageError>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Button,
|
Button,
|
||||||
withTheme2,
|
withTheme2,
|
||||||
Themeable2,
|
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { capitalCase } from 'change-case';
|
import { capitalCase } from 'change-case';
|
||||||
import { debounce, isUndefined, omitBy, pickBy } from 'lodash-es';
|
import { debounce, isUndefined, omitBy, pickBy } from 'lodash-es';
|
||||||
|
|
@ -38,12 +37,13 @@ import { parseFilters } from './RemoteFilters.helpers';
|
||||||
import { FilterOption } from './RemoteFilters.types';
|
import { FilterOption } from './RemoteFilters.types';
|
||||||
import { TimeRangePickerWrapper } from './TimeRangePickerWrapper';
|
import { TimeRangePickerWrapper } from './TimeRangePickerWrapper';
|
||||||
|
|
||||||
interface RemoteFiltersProps extends WithStoreProps, Themeable2 {
|
interface RemoteFiltersProps extends WithStoreProps {
|
||||||
onChange: (filters: Record<string, any>, isOnMount: boolean, invalidateFn: () => boolean) => void;
|
onChange: (filters: Record<string, any>, isOnMount: boolean, invalidateFn: () => boolean) => void;
|
||||||
query: KeyValue;
|
query: KeyValue;
|
||||||
page: PAGE;
|
page: PAGE;
|
||||||
grafanaTeamStore: GrafanaTeamStore;
|
grafanaTeamStore: GrafanaTeamStore;
|
||||||
extraInformation?: FilterExtraInformation;
|
extraInformation?: FilterExtraInformation;
|
||||||
|
theme: GrafanaTheme2;
|
||||||
extraFilters?: (state, setState, onFiltersValueChange) => React.ReactNode;
|
extraFilters?: (state, setState, onFiltersValueChange) => React.ReactNode;
|
||||||
skipFilterOptionFn?: (filterOption: FilterOption) => boolean;
|
skipFilterOptionFn?: (filterOption: FilterOption) => boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FC, useMemo } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
|
|
||||||
import { HorizontalGroup, LoadingPlaceholder } from '@grafana/ui';
|
import { LoadingPlaceholder, Stack } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
@ -177,9 +177,9 @@ export const Rotation: FC<RotationProps> = observer((props) => {
|
||||||
<Empty text={emptyText} />
|
<Empty text={emptyText} />
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<HorizontalGroup align="center" justify="center">
|
<Stack alignItems="center" justifyContent="center">
|
||||||
<LoadingPlaceholder text="Loading shifts..." />
|
<LoadingPlaceholder text="Loading shifts..." />
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,6 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import { Alert, Button, Field, Icon, IconButton, InlineSwitch, Select, Switch, Tooltip, Stack } from '@grafana/ui';
|
||||||
Alert,
|
|
||||||
Button,
|
|
||||||
Field,
|
|
||||||
HorizontalGroup,
|
|
||||||
Icon,
|
|
||||||
IconButton,
|
|
||||||
InlineSwitch,
|
|
||||||
Select,
|
|
||||||
Switch,
|
|
||||||
Tooltip,
|
|
||||||
VerticalGroup,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
|
|
@ -63,7 +51,7 @@ import {
|
||||||
toDateWithTimezoneOffsetAtMidnight,
|
toDateWithTimezoneOffsetAtMidnight,
|
||||||
} from 'pages/schedule/Schedule.helpers';
|
} from 'pages/schedule/Schedule.helpers';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
import { GRAFANA_HEADER_HEIGHT } from 'utils/consts';
|
import { GRAFANA_HEADER_HEIGHT, StackSize } from 'utils/consts';
|
||||||
import { useDebouncedCallback, useResize } from 'utils/hooks';
|
import { useDebouncedCallback, useResize } from 'utils/hooks';
|
||||||
|
|
||||||
import styles from './RotationForm.module.css';
|
import styles from './RotationForm.module.css';
|
||||||
|
|
@ -552,14 +540,14 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
||||||
>
|
>
|
||||||
<div className={cx('root')} data-testid="rotation-form">
|
<div className={cx('root')} data-testid="rotation-form">
|
||||||
<div>
|
<div>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<HorizontalGroup spacing="sm">
|
<Stack gap={StackSize.sm}>
|
||||||
{shiftId === 'new' && <Tag color={shiftColor}>New</Tag>}
|
{shiftId === 'new' && <Tag color={shiftColor}>New</Tag>}
|
||||||
<Text.Title editModalTitle="Rotation name" onTextChange={handleRotationNameChange} level={5} editable>
|
<Text.Title editModalTitle="Rotation name" onTextChange={handleRotationNameChange} level={5} editable>
|
||||||
{rotationName}
|
{rotationName}
|
||||||
</Text.Title>
|
</Text.Title>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
{shiftId !== 'new' && (
|
{shiftId !== 'new' && (
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|
@ -575,16 +563,16 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
||||||
tooltip={shiftId === 'new' ? 'Cancel' : 'Close'}
|
tooltip={shiftId === 'new' ? 'Cancel' : 'Close'}
|
||||||
onClick={onHide}
|
onClick={onHide}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
<div className={cx('container')}>
|
<div className={cx('container')}>
|
||||||
<div className={cx('content')}>
|
<div className={cx('content')}>
|
||||||
<VerticalGroup spacing="none">
|
<Stack direction="column" gap={StackSize.none}>
|
||||||
{hasUpdatedShift && (
|
{hasUpdatedShift && (
|
||||||
<Block bordered className={cx('updated-shift-info')}>
|
<Block bordered className={cx('updated-shift-info')}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<HorizontalGroup align="flex-start">
|
<Stack alignItems="flex-start">
|
||||||
<Icon name="info-circle" size="md"></Icon>
|
<Icon name="info-circle" size="md"></Icon>
|
||||||
<Text>
|
<Text>
|
||||||
This rotation is read-only because it has newer version.{' '}
|
This rotation is read-only because it has newer version.{' '}
|
||||||
|
|
@ -593,15 +581,15 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
instead
|
instead
|
||||||
</Text>
|
</Text>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Block>
|
</Block>
|
||||||
)}
|
)}
|
||||||
{!hasUpdatedShift && ended && (
|
{!hasUpdatedShift && ended && (
|
||||||
<div className={cx('updated-shift-info')}>
|
<div className={cx('updated-shift-info')}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Alert severity="info" title={(<Text>This rotation is over</Text>) as unknown as string} />
|
<Alert severity="info" title={(<Text>This rotation is over</Text>) as unknown as string} />
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={cx('two-fields')}>
|
<div className={cx('two-fields')}>
|
||||||
|
|
@ -623,7 +611,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
label={
|
label={
|
||||||
<HorizontalGroup spacing="xs">
|
<Stack gap={StackSize.xs}>
|
||||||
<Text type="primary" size="small">
|
<Text type="primary" size="small">
|
||||||
Ends
|
Ends
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -634,7 +622,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
||||||
onChange={handleChangeEndless}
|
onChange={handleChangeEndless}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
}
|
}
|
||||||
data-testid="rotation-end"
|
data-testid="rotation-end"
|
||||||
>
|
>
|
||||||
|
|
@ -658,14 +646,14 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
||||||
invalid={Boolean(errors.interval)}
|
invalid={Boolean(errors.interval)}
|
||||||
error={'Invalid recurrence period'}
|
error={'Invalid recurrence period'}
|
||||||
label={
|
label={
|
||||||
<HorizontalGroup spacing="sm">
|
<Stack gap={StackSize.sm}>
|
||||||
<Text type="primary" size="small">
|
<Text type="primary" size="small">
|
||||||
Recurrence period
|
Recurrence period
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip content="Time interval when users shifts are rotated. Shifts active period can be customised by days of the week and hours during a day.">
|
<Tooltip content="Time interval when users shifts are rotated. Shifts active period can be customised by days of the week and hours during a day.">
|
||||||
<Icon name="info-circle" size="md"></Icon>
|
<Icon name="info-circle" size="md"></Icon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -687,11 +675,11 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
<VerticalGroup spacing="md">
|
<Stack direction="column" gap={StackSize.md}>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<HorizontalGroup align="flex-start">
|
<Stack alignItems="flex-start">
|
||||||
<Switch disabled={disabled} value={isMaskedByWeekdays} onChange={onMaskedByWeekdaysSwitch} />
|
<Switch disabled={disabled} value={isMaskedByWeekdays} onChange={onMaskedByWeekdaysSwitch} />
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Text type="secondary">Mask by weekdays</Text>
|
<Text type="secondary">Mask by weekdays</Text>
|
||||||
{isMaskedByWeekdays && (
|
{isMaskedByWeekdays && (
|
||||||
<DaysSelector
|
<DaysSelector
|
||||||
|
|
@ -702,16 +690,16 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
|
|
||||||
<HorizontalGroup align="flex-start">
|
<Stack alignItems="flex-start">
|
||||||
<Switch
|
<Switch
|
||||||
disabled={isSelectedPartOfDayDisabled()}
|
disabled={isSelectedPartOfDayDisabled()}
|
||||||
value={isLimitShiftEnabled}
|
value={isLimitShiftEnabled}
|
||||||
onChange={onLimitShiftSwitch}
|
onChange={onLimitShiftSwitch}
|
||||||
/>
|
/>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<Text type="secondary">Limit each shift length</Text>
|
<Text type="secondary">Limit each shift length</Text>
|
||||||
{isLimitShiftEnabled && (
|
{isLimitShiftEnabled && (
|
||||||
<ShiftPeriod
|
<ShiftPeriod
|
||||||
|
|
@ -736,17 +724,17 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
||||||
will repeat every day
|
will repeat every day
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
<div style={{ marginTop: '16px' }}>
|
<div style={{ marginTop: '16px' }}>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Text size="small">Users</Text>
|
<Text size="small">Users</Text>
|
||||||
<Tooltip content="By default each new user creates new rotation group. You can customise groups by dragging.">
|
<Tooltip content="By default each new user creates new rotation group. You can customise groups by dragging.">
|
||||||
<Icon name="info-circle" size="md" />
|
<Icon name="info-circle" size="md" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
<UserGroups
|
<UserGroups
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
@ -763,15 +751,15 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
||||||
)}
|
)}
|
||||||
showError={Boolean(errors.rolling_users)}
|
showError={Boolean(errors.rolling_users)}
|
||||||
/>
|
/>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
Current timezone: <Text type="primary">{store.timezoneStore.selectedTimezoneLabel}</Text>
|
Current timezone: <Text type="primary">{store.timezoneStore.selectedTimezoneLabel}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
{shiftId !== 'new' && (
|
{shiftId !== 'new' && (
|
||||||
<Tooltip content="Stop the current rotation and start a new one">
|
<Tooltip content="Stop the current rotation and start a new one">
|
||||||
<Button disabled={disabled} variant="secondary" onClick={updateAsNew}>
|
<Button disabled={disabled} variant="secondary" onClick={updateAsNew}>
|
||||||
|
|
@ -790,8 +778,8 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
@ -929,9 +917,9 @@ const ShiftPeriod = ({
|
||||||
}, [unitToCreate]);
|
}, [unitToCreate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
{timeUnits.map((unit, index: number, arr) => (
|
{timeUnits.map((unit, index: number, arr) => (
|
||||||
<HorizontalGroup key={unit.unit}>
|
<Stack key={unit.unit}>
|
||||||
<TimeUnitSelector
|
<TimeUnitSelector
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
unit={unit.unit}
|
unit={unit.unit}
|
||||||
|
|
@ -960,7 +948,7 @@ const ShiftPeriod = ({
|
||||||
onClick={handleTimeUnitAdd}
|
onClick={handleTimeUnitAdd}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
))}
|
))}
|
||||||
{timeUnits.length === 0 && unitToCreate !== undefined && (
|
{timeUnits.length === 0 && unitToCreate !== undefined && (
|
||||||
<Button disabled={disabled} variant="secondary" icon="plus" size="sm" onClick={handleTimeUnitAdd}>
|
<Button disabled={disabled} variant="secondary" icon="plus" size="sm" onClick={handleTimeUnitAdd}>
|
||||||
|
|
@ -969,6 +957,6 @@ const ShiftPeriod = ({
|
||||||
)}
|
)}
|
||||||
<Text type="secondary">({duration || '0m'})</Text>
|
<Text type="secondary">({duration || '0m'})</Text>
|
||||||
{errors.shift_end && <Text type="danger">Shift length must be greater than zero</Text>}
|
{errors.shift_end && <Text type="danger">Shift length must be greater than zero</Text>}
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { IconButton, VerticalGroup, HorizontalGroup, Field, Button, useTheme2 } from '@grafana/ui';
|
import { IconButton, Stack, Field, Button, useTheme2 } from '@grafana/ui';
|
||||||
import cn from 'classnames/bind';
|
import cn from 'classnames/bind';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
|
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
|
||||||
|
|
@ -16,6 +16,7 @@ import { Schedule, Shift } from 'models/schedule/schedule.types';
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||||
import { getDateTime, getUTCString, toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers';
|
import { getDateTime, getUTCString, toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
|
import { StackSize } from 'utils/consts';
|
||||||
import { useDebouncedCallback, useResize } from 'utils/hooks';
|
import { useDebouncedCallback, useResize } from 'utils/hooks';
|
||||||
|
|
||||||
import { getDraggableModalCoordinatesOnInit } from './RotationForm.helpers';
|
import { getDraggableModalCoordinatesOnInit } from './RotationForm.helpers';
|
||||||
|
|
@ -219,15 +220,15 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
||||||
</Draggable>
|
</Draggable>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<HorizontalGroup spacing="sm">
|
<Stack gap={StackSize.sm}>
|
||||||
{shiftId === 'new' && <Tag color={shiftColor}>New</Tag>}
|
{shiftId === 'new' && <Tag color={shiftColor}>New</Tag>}
|
||||||
<Text.Title onTextChange={handleRotationNameChange} level={5} editable>
|
<Text.Title onTextChange={handleRotationNameChange} level={5} editable>
|
||||||
{rotationName}
|
{rotationName}
|
||||||
</Text.Title>
|
</Text.Title>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
{shiftId !== 'new' && (
|
{shiftId !== 'new' && (
|
||||||
<WithConfirm title="Are you sure you want to delete override?">
|
<WithConfirm title="Are you sure you want to delete override?">
|
||||||
<IconButton variant="secondary" tooltip="Delete" name="trash-alt" onClick={handleDeleteClick} />
|
<IconButton variant="secondary" tooltip="Delete" name="trash-alt" onClick={handleDeleteClick} />
|
||||||
|
|
@ -240,13 +241,13 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
||||||
tooltip={shiftId === 'new' ? 'Cancel' : 'Close'}
|
tooltip={shiftId === 'new' ? 'Cancel' : 'Close'}
|
||||||
onClick={onHide}
|
onClick={onHide}
|
||||||
/>
|
/>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
|
|
||||||
<div className={cx('container')}>
|
<div className={cx('container')}>
|
||||||
<div className={cx('override-form-content')} data-testid="override-inputs">
|
<div className={cx('override-form-content')} data-testid="override-inputs">
|
||||||
<VerticalGroup>
|
<Stack direction="column">
|
||||||
<HorizontalGroup align="flex-start">
|
<Stack alignItems="flex-start">
|
||||||
<Field
|
<Field
|
||||||
className={cx('date-time-picker')}
|
className={cx('date-time-picker')}
|
||||||
data-testid="override-start"
|
data-testid="override-start"
|
||||||
|
|
@ -282,7 +283,7 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
||||||
error={errors.shift_end}
|
error={errors.shift_end}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
|
|
||||||
<UserGroups
|
<UserGroups
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
@ -299,20 +300,20 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
||||||
)}
|
)}
|
||||||
showError={Boolean(errors.rolling_users)}
|
showError={Boolean(errors.rolling_users)}
|
||||||
/>
|
/>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HorizontalGroup justify="space-between">
|
<Stack justifyContent="space-between">
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
Current timezone: <Text type="primary">{store.timezoneStore.selectedTimezoneLabel}</Text>
|
Current timezone: <Text type="primary">{store.timezoneStore.selectedTimezoneLabel}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<HorizontalGroup>
|
<Stack>
|
||||||
<Button variant="primary" onClick={handleCreate} disabled={disabled || !isFormValid}>
|
<Button variant="primary" onClick={handleCreate} disabled={disabled || !isFormValid}>
|
||||||
{shiftId === 'new' ? 'Create' : 'Update'}
|
{shiftId === 'new' ? 'Create' : 'Update'}
|
||||||
</Button>
|
</Button>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</HorizontalGroup>
|
</Stack>
|
||||||
</VerticalGroup>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue