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:
|
||||
matrix:
|
||||
grafana_version:
|
||||
- 10.1.7
|
||||
- 10.3.3
|
||||
- 10.3.0
|
||||
- latest
|
||||
fail-fast: false
|
||||
# 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)
|
||||
|
|
|
|||
7
.github/workflows/linting-and-tests.yml
vendored
7
.github/workflows/linting-and-tests.yml
vendored
|
|
@ -242,11 +242,8 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
grafana_version:
|
||||
- 10.1.7
|
||||
- 10.3.3
|
||||
# TODO: fix issues with running e2e tests against Grafana v10.2.x and latest
|
||||
# - 10.2.4
|
||||
# - latest
|
||||
- 10.3.0
|
||||
- latest
|
||||
fail-fast: false
|
||||
with:
|
||||
grafana_version: ${{ matrix.grafana_version }}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ module.exports = {
|
|||
{
|
||||
files: ['src/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
'deprecation/deprecation': 'off',
|
||||
'deprecation/deprecation': 'warn',
|
||||
},
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ test.describe('maintenance mode works', () => {
|
|||
await page.waitForTimeout(2000);
|
||||
const integrationSettingsPopupElement = page
|
||||
.getByTestId('integration-settings-context-menu-wrapper')
|
||||
.getByRole('img');
|
||||
.locator('svg');
|
||||
|
||||
await integrationSettingsPopupElement.click();
|
||||
/**
|
||||
* 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
|
||||
await webhooksFormDivs.locator('[name=name]').fill(WEBHOOK_NAME);
|
||||
|
||||
// Select team
|
||||
await page.getByLabel('New Outgoing Webhook').getByRole('img').nth(1).click(); // Open team dropdown
|
||||
await page.getByLabel('Select options menu').getByText('No team').click(); // Select "No team"
|
||||
// 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();
|
||||
|
||||
// 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();
|
||||
|
||||
// Select integration
|
||||
|
|
|
|||
|
|
@ -18,8 +18,12 @@ const createWebhook = async ({ page, webhookName, webhookUrl }) => {
|
|||
|
||||
await page.keyboard.insertText(webhookUrl);
|
||||
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 clickButton({ page, buttonText: 'Create' });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ test.describe('Plugin configuration', () => {
|
|||
adminRolePage: { page },
|
||||
}) => {
|
||||
await goToGrafanaPage(page, PLUGIN_CONFIG);
|
||||
await page.waitForLoadState('networkidle');
|
||||
const correctURLAppliedByDefault = await page.getByTestId('oncall-api-url-input').inputValue();
|
||||
|
||||
// show client-side validation errors
|
||||
|
|
@ -27,6 +28,7 @@ test.describe('Plugin configuration', () => {
|
|||
|
||||
// apply back correct url and verify plugin connected again
|
||||
await urlInput.fill(correctURLAppliedByDefault);
|
||||
await page.waitForTimeout(500);
|
||||
await page.getByTestId('connect-plugin').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
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');
|
||||
|
||||
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}
|
||||
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
|
||||
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');
|
||||
|
||||
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}
|
||||
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
|
||||
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');
|
||||
|
||||
// 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();
|
||||
|
||||
// Selected timezone and local time is correctly displayed
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { cx } from '@emotion/css';
|
||||
import { VerticalGroup, useStyles2 } from '@grafana/ui';
|
||||
import { Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { Block } from 'components/GBlock/Block';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import { getCardButtonStyles } from './CardButton.styles';
|
||||
|
||||
|
|
@ -30,10 +31,10 @@ export const CardButton: FC<CardButtonProps> = (props) => {
|
|||
>
|
||||
<div className={styles.icon}>{icon}</div>
|
||||
<div className={styles.meta}>
|
||||
<VerticalGroup spacing="xs">
|
||||
<Stack gap={StackSize.xs}>
|
||||
<Text type="secondary">{description}</Text>
|
||||
<Text.Title level={1}>{title}</Text.Title>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</Block>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
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 { bem, getUtilStyles } from 'styles/utils.styles';
|
||||
|
||||
import { Block } from 'components/GBlock/Block';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { StackSize } from 'utils/consts';
|
||||
import { openNotification } from 'utils/utils';
|
||||
|
||||
import { CheatSheetInterface, CheatSheetItem } from './CheatSheet.config';
|
||||
|
|
@ -26,11 +27,11 @@ export const CheatSheet = (props: CheatSheetProps) => {
|
|||
return (
|
||||
<div className={styles.cheatsheetContainer}>
|
||||
<div className={styles.cheatsheetInnerContainer}>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Stack direction="column">
|
||||
<Stack justifyContent="space-between">
|
||||
<Text strong>{cheatSheetName} cheatsheet</Text>
|
||||
<IconButton aria-label="Close" name="times" onClick={onClose} />
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
<Text type="secondary">{cheatSheetData.description}</Text>
|
||||
<div className={utils.width100}>
|
||||
{cheatSheetData.fields?.map((field: CheatSheetItem) => {
|
||||
|
|
@ -41,7 +42,7 @@ export const CheatSheet = (props: CheatSheetProps) => {
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -60,7 +61,7 @@ const CheatSheetListItem = (props: CheatSheetListItemProps) => {
|
|||
{field.listItems?.map((item, key) => {
|
||||
return (
|
||||
<div key={key}>
|
||||
<VerticalGroup spacing="md">
|
||||
<Stack direction="column" gap={StackSize.md}>
|
||||
{item.listItemName && (
|
||||
<li style={{ margin: '0 0 0 4px' }}>
|
||||
<Text>{item.listItemName}</Text>
|
||||
|
|
@ -69,18 +70,18 @@ const CheatSheetListItem = (props: CheatSheetListItemProps) => {
|
|||
{item.codeExample && (
|
||||
<div className={bem(styles.cheatsheetItem, 'small')}>
|
||||
<Block bordered fullWidth withBackground>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Stack justifyContent="space-between">
|
||||
<Text type="link" className={styles.code}>
|
||||
{item.codeExample}
|
||||
</Text>
|
||||
<CopyToClipboard text={item.codeExample} onCopy={() => openNotification('Example copied')}>
|
||||
<IconButton aria-label="Copy" name="copy" />
|
||||
</CopyToClipboard>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Block>
|
||||
</div>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
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 { StackSize } from 'utils/consts';
|
||||
|
||||
interface CursorPaginationProps {
|
||||
current: string;
|
||||
|
|
@ -30,8 +31,8 @@ export const CursorPagination: FC<CursorPaginationProps> = (props) => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<HorizontalGroup spacing="md" justify="flex-end">
|
||||
<HorizontalGroup>
|
||||
<Stack gap={StackSize.md} justifyContent="flex-end">
|
||||
<Stack>
|
||||
<Text type="secondary">Items per list</Text>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
|
|
@ -39,8 +40,8 @@ export const CursorPagination: FC<CursorPaginationProps> = (props) => {
|
|||
value={itemsPerPage}
|
||||
onChange={onChangeItemsPerPageCallback}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Button
|
||||
aria-label="previous"
|
||||
size="sm"
|
||||
|
|
@ -66,7 +67,7 @@ export const CursorPagination: FC<CursorPaginationProps> = (props) => {
|
|||
>
|
||||
<Icon name="angle-right" />
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
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 { Text } from 'components/Text/Text';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
interface FullPageErrorProps {
|
||||
children?: React.ReactNode;
|
||||
|
|
@ -21,12 +22,12 @@ export const FullPageError: FC<FullPageErrorProps> = ({
|
|||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<VerticalGroup align="center" spacing="md">
|
||||
<Stack direction="column" alignItems="center" gap={StackSize.md}>
|
||||
<img src={errorSVG} alt="" />
|
||||
<Text.Title level={3}>{title}</Text.Title>
|
||||
{subtitle && <Text type="secondary">{subtitle}</Text>}
|
||||
{children}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,18 +1,7 @@
|
|||
import React, { useEffect, useReducer } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
HorizontalGroup,
|
||||
Icon,
|
||||
IconButton,
|
||||
Input,
|
||||
RadioButtonGroup,
|
||||
Select,
|
||||
Tooltip,
|
||||
VerticalGroup,
|
||||
} from '@grafana/ui';
|
||||
import { Button, Drawer, Icon, IconButton, Input, RadioButtonGroup, Select, Tooltip, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
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 styles from 'pages/integration/Integration.module.scss';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { GENERIC_ERROR } from 'utils/consts';
|
||||
import { GENERIC_ERROR, StackSize } from 'utils/consts';
|
||||
import { openErrorNotification, openNotification } from 'utils/utils';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
|
@ -122,33 +111,33 @@ export const IntegrationContactPoint: React.FC<{
|
|||
/>
|
||||
|
||||
<div className={cx('contactpoints__connect')}>
|
||||
<VerticalGroup spacing="md">
|
||||
<Stack direction="column" gap={StackSize.md}>
|
||||
<div
|
||||
className={cx('contactpoints__connect-toggler')}
|
||||
onClick={() => setState({ isConnectOpen: !isConnectOpen })}
|
||||
>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="xs" align="center">
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack gap={StackSize.xs} alignItems="center">
|
||||
<Text type="primary">Grafana Alerting Contact point</Text>
|
||||
<Icon name="info-circle" />
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
|
||||
{isConnectOpen ? <Icon name="arrow-down" /> : <Icon name="arrow-right" />}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
{renderConnectSection()}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
)}
|
||||
|
||||
<HorizontalGroup spacing="md">
|
||||
<Stack gap={StackSize.md}>
|
||||
<IntegrationTag>Contact point</IntegrationTag>
|
||||
|
||||
{contactPoints?.length ? (
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<Text type="primary">
|
||||
{contactPoints.length} contact point{contactPoints.length === 1 ? '' : 's'} connected
|
||||
</Text>
|
||||
|
|
@ -163,16 +152,16 @@ export const IntegrationContactPoint: React.FC<{
|
|||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
) : (
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Stack gap={StackSize.xs}>
|
||||
{renderExclamationIcon()}
|
||||
<Text type="primary" data-testid="integration-escalation-chain-not-selected">
|
||||
Connect Alerting Contact point to receive alerts
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
|
|
@ -194,7 +183,7 @@ export const IntegrationContactPoint: React.FC<{
|
|||
}
|
||||
|
||||
return (
|
||||
<VerticalGroup spacing="md">
|
||||
<Stack direction="column" gap={StackSize.md}>
|
||||
<RadioButtonGroup
|
||||
options={radioOptions}
|
||||
value={isExistingContactPoint ? 'existing' : 'new'}
|
||||
|
|
@ -233,7 +222,7 @@ export const IntegrationContactPoint: React.FC<{
|
|||
/>
|
||||
)}
|
||||
|
||||
<HorizontalGroup align="center">
|
||||
<Stack alignItems="center">
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!selectedAlertManager || !selectedContactPoint || isLoading}
|
||||
|
|
@ -245,8 +234,8 @@ export const IntegrationContactPoint: React.FC<{
|
|||
Cancel
|
||||
</Button>
|
||||
{isLoading && <Icon name="fa fa-spinner" size="md" className={cx('loadingPlaceholder')} />}
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +252,7 @@ export const IntegrationContactPoint: React.FC<{
|
|||
};
|
||||
|
||||
return (
|
||||
<HorizontalGroup spacing="md">
|
||||
<Stack gap={StackSize.md}>
|
||||
<IconButton
|
||||
aria-label="Alert Manager"
|
||||
name="external-link-alt"
|
||||
|
|
@ -278,23 +267,23 @@ export const IntegrationContactPoint: React.FC<{
|
|||
title={`Disconnect Contact point`}
|
||||
confirmText="Disconnect"
|
||||
description={
|
||||
<VerticalGroup spacing="md">
|
||||
<Stack direction="column" gap={StackSize.md}>
|
||||
<Text type="primary">
|
||||
When the contact point will be disconnected, the Integration will no longer receive alerts for it.
|
||||
</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} />
|
||||
</WithConfirm>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function renderContactPointName(item: ContactPoint) {
|
||||
return (
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Stack gap={StackSize.xs}>
|
||||
<Text type="primary">{item.contactPoint}</Text>
|
||||
|
||||
{!item.notificationConnected && (
|
||||
|
|
@ -305,7 +294,7 @@ export const IntegrationContactPoint: React.FC<{
|
|||
{renderExclamationIcon()}
|
||||
</Tooltip>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
|
||||
import { Icon, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { noop } from 'lodash-es';
|
||||
|
||||
|
|
@ -11,6 +11,7 @@ import { Text } from 'components/Text/Text';
|
|||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import styles from 'pages/integration/Integration.module.scss';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
|
|
@ -50,10 +51,10 @@ export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveCha
|
|||
className={cx('u-pull-right')}
|
||||
>
|
||||
<Text type="link" size="small">
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
How it works
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Text>
|
||||
</a>
|
||||
</>
|
||||
|
|
@ -74,10 +75,10 @@ export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveCha
|
|||
className={cx('u-pull-right')}
|
||||
>
|
||||
<Text type="link" size="small">
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
How to connect
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Text>
|
||||
</a>
|
||||
</>
|
||||
|
|
@ -98,14 +99,14 @@ export const IntegrationHowToConnect: React.FC<{ id: ApiSchemas['AlertReceiveCha
|
|||
};
|
||||
|
||||
return (
|
||||
<VerticalGroup justify={'flex-start'} spacing={'xs'}>
|
||||
<Stack direction="column" justifyContent={'flex-start'} gap={StackSize.xs}>
|
||||
{!hasAlerts && (
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Stack gap={StackSize.xs}>
|
||||
<Icon name="fa fa-spinner" size="md" className={cx('loadingPlaceholder')} />
|
||||
<Text type={'primary'}>No alerts yet</Text> {callToAction()}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
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 { StackSize } from 'utils/consts';
|
||||
|
||||
import { getIntegrationInputFieldStyles } from './IntegrationInputField.styles';
|
||||
|
||||
|
|
@ -38,11 +39,11 @@ export const IntegrationInputField: React.FC<IntegrationInputFieldProps> = ({
|
|||
<div className={styles.inputContainer}>{renderInputField()}</div>
|
||||
|
||||
<div className={cx(styles.icons, iconsClassName)}>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Stack gap={StackSize.xs}>
|
||||
{showEye && <IconButton aria-label="Reveal" name={'eye'} size={'xs'} onClick={onInputReveal} />}
|
||||
{showCopy && <CopyToClipboardIcon text={value} iconButtonProps={{ size: 'xs' }} />}
|
||||
{showExternal && <IconButton aria-label="Open" name={'external-link-alt'} size={'xs'} onClick={onOpen} />}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { HorizontalGroup } from '@grafana/ui';
|
||||
import { Stack } from '@grafana/ui';
|
||||
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import { IntegrationLogo, IntegrationLogoProps } from './IntegrationLogo';
|
||||
|
||||
|
|
@ -11,8 +12,8 @@ interface IntegrationLogoWithTitleProps {
|
|||
}
|
||||
|
||||
export const IntegrationLogoWithTitle: FC<IntegrationLogoWithTitleProps> = ({ integration }) => (
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Stack gap={StackSize.xs}>
|
||||
<IntegrationLogo scale={0.08} integration={integration} />
|
||||
<Text type="primary">{integration?.display_name}</Text>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 CopyToClipboard from 'react-copy-to-clipboard';
|
||||
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 styles from 'pages/integration/Integration.module.scss';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { StackSize } from 'utils/consts';
|
||||
import { openNotification } from 'utils/utils';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
|
@ -42,18 +43,18 @@ export const IntegrationSendDemoAlertModal: React.FC<IntegrationSendDemoPayloadM
|
|||
isOpen={isOpen}
|
||||
onDismiss={onHideOrCancel}
|
||||
title={
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<Text.Title level={4}>
|
||||
Send demo alert to integration: {''}
|
||||
<strong>
|
||||
<Emoji text={alertReceiveChannel.verbal_name} />
|
||||
</strong>
|
||||
</Text.Title>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Stack direction="column">
|
||||
<Stack gap={StackSize.xs}>
|
||||
<Text type={'secondary'}>Alert Payload</Text>
|
||||
<Tooltip
|
||||
content={
|
||||
|
|
@ -66,7 +67,7 @@ export const IntegrationSendDemoAlertModal: React.FC<IntegrationSendDemoPayloadM
|
|||
>
|
||||
<Icon name={'info-circle'} />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
|
||||
<div className={cx('integration__payloadInput')}>
|
||||
<MonacoEditor
|
||||
|
|
@ -82,7 +83,7 @@ export const IntegrationSendDemoAlertModal: React.FC<IntegrationSendDemoPayloadM
|
|||
/>
|
||||
</div>
|
||||
|
||||
<HorizontalGroup justify={'flex-end'} spacing={'md'}>
|
||||
<Stack justifyContent={'flex-end'} gap={StackSize.md}>
|
||||
<Button variant={'secondary'} onClick={onHideOrCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
@ -92,8 +93,8 @@ export const IntegrationSendDemoAlertModal: React.FC<IntegrationSendDemoPayloadM
|
|||
<Button variant={'primary'} onClick={onSendAlert} data-testid="submit-send-alert">
|
||||
Send Alert
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
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 { TooltipBadge } from 'components/TooltipBadge/TooltipBadge';
|
||||
import { LabelKeyValue } from 'models/label/label.types';
|
||||
import { components } from 'network/oncall-api/autogenerated-api.types';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
interface LabelsTooltipBadgeProps {
|
||||
labels: LabelKeyValue[];
|
||||
|
|
@ -21,9 +22,9 @@ export const LabelsTooltipBadge: FC<LabelsTooltipBadgeProps> = ({ labels, onClic
|
|||
addPadding
|
||||
text={labels?.length}
|
||||
tooltipContent={
|
||||
<VerticalGroup spacing="sm">
|
||||
<Stack direction="column" gap={StackSize.sm}>
|
||||
{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} />
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
@ -32,9 +33,9 @@ export const LabelsTooltipBadge: FC<LabelsTooltipBadgeProps> = ({ labels, onClic
|
|||
variant="secondary"
|
||||
onClick={() => onClick(label)}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
))}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
) : null;
|
||||
|
|
@ -47,16 +48,16 @@ interface LabelBadgesProps {
|
|||
export const LabelBadges: React.FC<LabelBadgesProps> = ({ labels = [], maxCount = 3 }) => {
|
||||
const renderer = (values: LabelBadgesProps['labels']) => {
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
{values.map((label) => (
|
||||
<LabelTag key={label.key.id} label={label.key.name} value={label.value.name} />
|
||||
))}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<HorizontalGroup spacing="sm">
|
||||
<Stack gap={StackSize.sm}>
|
||||
{renderer(labels.slice(0, 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>
|
||||
</Tooltip>
|
||||
</RenderConditionally>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
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 { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { getUtilStyles } from 'styles/utils.styles';
|
||||
|
|
@ -70,7 +70,7 @@ export const ManualAlertGroup: FC<ManualAlertGroupProps> = observer(({ onCreate,
|
|||
|
||||
return (
|
||||
<Drawer scrollableContent title="New escalation" onClose={onHideDrawer} closeOnMaskClick={false} width="70%">
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<FormProvider {...formMethods}>
|
||||
<form id="Manual Alert Group" onSubmit={handleSubmit(onSubmit)} className={utilStyles.width100}>
|
||||
<Controller
|
||||
|
|
@ -87,18 +87,18 @@ export const ManualAlertGroup: FC<ManualAlertGroupProps> = observer(({ onCreate,
|
|||
<AddResponders mode="create" />
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Stack justifyContent="flex-end">
|
||||
<Button variant="secondary" onClick={onHideDrawer}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!formIsSubmittable}>
|
||||
Create
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useCallback, useState } from 'react';
|
||||
|
||||
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 { Text } from 'components/Text/Text';
|
||||
|
|
@ -9,6 +9,7 @@ import { ScheduleForm } from 'containers/ScheduleForm/ScheduleForm';
|
|||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { Schedule, ScheduleType } from 'models/schedule/schedule.types';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
interface NewScheduleSelectorProps {
|
||||
onHide: () => void;
|
||||
|
|
@ -34,52 +35,52 @@ export const NewScheduleSelector: FC<NewScheduleSelectorProps> = ({ onHide, onCr
|
|||
return (
|
||||
<Drawer scrollableContent title="Create new schedule" onClose={onHide} closeOnMaskClick={false}>
|
||||
<div className={styles.content}>
|
||||
<VerticalGroup spacing="lg">
|
||||
<Stack direction="column" gap={StackSize.lg}>
|
||||
<Block bordered withBackground className={styles.block}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="md">
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack gap={StackSize.md}>
|
||||
<Icon name="calendar-alt" size="xl" />
|
||||
<VerticalGroup spacing="none">
|
||||
<Stack direction="column" gap={StackSize.none}>
|
||||
<Text type="primary" size="large">
|
||||
Set up on-call rotation schedule
|
||||
</Text>
|
||||
<Text type="secondary">Configure rotations and shifts directly in Grafana On-Call</Text>
|
||||
</VerticalGroup>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<WithPermissionControlTooltip userAction={UserActions.SchedulesWrite}>
|
||||
<Button variant="primary" icon="plus" onClick={getCreateScheduleClickHandler(ScheduleType.API)}>
|
||||
Create
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Block>
|
||||
<Block bordered withBackground className={styles.block}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="md">
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack gap={StackSize.md}>
|
||||
<Icon name="download-alt" size="xl" />
|
||||
<VerticalGroup spacing="none">
|
||||
<Stack direction="column" gap={StackSize.none}>
|
||||
<Text type="primary" size="large">
|
||||
Import schedule from iCal Url
|
||||
</Text>
|
||||
<Text type="secondary">Import rotations and shifts from your calendar app</Text>
|
||||
</VerticalGroup>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Button variant="secondary" icon="plus" onClick={getCreateScheduleClickHandler(ScheduleType.Ical)}>
|
||||
Create
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Block>
|
||||
<Block bordered withBackground className={styles.block}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="md">
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack gap={StackSize.md}>
|
||||
<Icon name="cog" size="xl" />
|
||||
<VerticalGroup spacing="none">
|
||||
<Stack direction="column" gap={StackSize.none}>
|
||||
<Text type="primary" size="large">
|
||||
Create schedule by API
|
||||
</Text>
|
||||
<Text type="secondary">Use API or Terraform to manage rotations</Text>
|
||||
</VerticalGroup>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<a
|
||||
target="_blank"
|
||||
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>
|
||||
</a>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Block>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import React, { ComponentProps, FC } from 'react';
|
||||
|
||||
import { HorizontalGroup, Icon, Tooltip } from '@grafana/ui';
|
||||
import { Icon, Stack, Tooltip } from '@grafana/ui';
|
||||
|
||||
interface NonExistentUserNameProps {
|
||||
justify?: ComponentProps<typeof HorizontalGroup>['justify'];
|
||||
justify?: ComponentProps<typeof Stack>['justifyContent'];
|
||||
userName?: string;
|
||||
}
|
||||
|
||||
const NonExistentUserName: FC<NonExistentUserNameProps> = ({ justify = 'space-between', userName }) => (
|
||||
<HorizontalGroup justify={justify}>
|
||||
<Stack justifyContent={justify}>
|
||||
<span>Missing user</span>
|
||||
<Tooltip content={`${userName || 'User'} } is not found or doesn't have permission to participate in the rotation`}>
|
||||
<Icon name="exclamation-triangle" />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
export default NonExistentUserName;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ import React, { useEffect } from 'react';
|
|||
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { VerticalGroup, useStyles2 } from '@grafana/ui';
|
||||
import { Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { PluginLink } from 'components/PluginLink/PluginLink';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { StackSize } from 'utils/consts';
|
||||
import { openWarningNotification } from 'utils/utils';
|
||||
|
||||
export interface PageBaseState {
|
||||
|
|
@ -53,7 +54,7 @@ export const PageErrorHandlingWrapper = function ({
|
|||
|
||||
return (
|
||||
<div className={styles.notFound}>
|
||||
<VerticalGroup spacing="lg" align="center">
|
||||
<Stack direction="column" gap={StackSize.lg} alignItems="center">
|
||||
<Text.Title level={1} className={styles.errorCode}>
|
||||
403
|
||||
</Text.Title>
|
||||
|
|
@ -66,7 +67,7 @@ export const PageErrorHandlingWrapper = function ({
|
|||
<Text type="secondary">
|
||||
Or return to the <PluginLink query={{ page: pageName }}>{objectName} list</PluginLink>
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { ChangeEvent } from 'react';
|
||||
|
||||
import { cx } from '@emotion/css';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Button, Input, Select, IconButton, withTheme2, Themeable2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Button, Input, Select, IconButton, withTheme2 } from '@grafana/ui';
|
||||
import { isNumber } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
import moment from 'moment-timezone';
|
||||
|
|
@ -57,7 +57,9 @@ interface EscalationPolicyBaseProps {
|
|||
|
||||
// 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
|
||||
export interface EscalationPolicyProps extends EscalationPolicyBaseProps, ElementSortableProps, Themeable2 {}
|
||||
export interface EscalationPolicyProps extends EscalationPolicyBaseProps, ElementSortableProps {
|
||||
theme: GrafanaTheme2;
|
||||
}
|
||||
|
||||
@observer
|
||||
class _EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
|
||||
import { css, cx } from '@emotion/css';
|
||||
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 { 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 { PolicyNote } from './PolicyNote';
|
||||
|
||||
export interface NotificationPolicyProps extends Themeable2 {
|
||||
export interface NotificationPolicyProps {
|
||||
theme: GrafanaTheme2;
|
||||
data: NotificationPolicyType;
|
||||
slackTeamIdentity?: {
|
||||
general_log_channel_pk: Channel['id'];
|
||||
|
|
@ -46,28 +47,18 @@ export interface NotificationPolicyProps extends Themeable2 {
|
|||
}
|
||||
|
||||
export class NotificationPolicy extends React.Component<NotificationPolicyProps, any> {
|
||||
private styles: ReturnType<typeof getStyles>;
|
||||
|
||||
constructor(props: NotificationPolicyProps) {
|
||||
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() {
|
||||
const { data, notificationChoices, number, color, userAction, isDisabled } = this.props;
|
||||
const { data, notificationChoices, number, color, userAction, isDisabled, theme } = this.props;
|
||||
const { id, step } = data;
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<Timeline.Item className={cx(this.styles.root)} number={number} backgroundHexNumber={color}>
|
||||
<div className={cx(this.styles.step)}>
|
||||
<Timeline.Item className={cx(styles.root)} number={number} backgroundHexNumber={color}>
|
||||
<div className={cx(styles.step)}>
|
||||
{!isDisabled && (
|
||||
<WithPermissionControlTooltip userAction={userAction}>
|
||||
<DragHandle />
|
||||
|
|
@ -75,7 +66,7 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
|
|||
)}
|
||||
<WithPermissionControlTooltip userAction={userAction}>
|
||||
<Select
|
||||
className={cx(this.styles.select, this.styles.control)}
|
||||
className={cx(styles.select, styles.control)}
|
||||
onChange={this._getOnChangeHandler('step')}
|
||||
value={step}
|
||||
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}>
|
||||
<IconButton
|
||||
aria-label="Remove"
|
||||
className={cx(this.styles.control)}
|
||||
className={cx(styles.control)}
|
||||
name="trash-alt"
|
||||
onClick={this._getDeleteClickHandler(id)}
|
||||
variant="secondary"
|
||||
|
|
@ -185,9 +176,11 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
|
|||
}
|
||||
|
||||
private _renderWaitDelays(disabled: boolean) {
|
||||
const { data, userAction } = this.props;
|
||||
const { data, userAction, theme } = this.props;
|
||||
const { wait_delay } = data;
|
||||
|
||||
const styles = getStyles(theme);
|
||||
|
||||
const optionsList = [...POLICY_DURATION_LIST_MINUTES];
|
||||
|
||||
const waitDelayInSeconds = parseFloat(wait_delay);
|
||||
|
|
@ -200,10 +193,10 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
|
|||
|
||||
return (
|
||||
<WithPermissionControlTooltip userAction={userAction}>
|
||||
<div className={this.styles.container}>
|
||||
<div className={styles.container}>
|
||||
<Select
|
||||
key="wait-delay"
|
||||
className={cx(this.styles.delay, this.styles.control)}
|
||||
className={cx(styles.delay, styles.control)}
|
||||
value={wait_delay ? optionValue : undefined}
|
||||
disabled={disabled}
|
||||
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) {
|
||||
const { data, notifyByOptions = [], userAction } = this.props;
|
||||
const { data, notifyByOptions = [], theme, userAction } = this.props;
|
||||
const { notify_by } = data;
|
||||
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<WithPermissionControlTooltip userAction={userAction}>
|
||||
<Select
|
||||
key="notify_by"
|
||||
placeholder="Notify by"
|
||||
className={cx(this.styles.select, this.styles.control)}
|
||||
className={cx(styles.select, styles.control)}
|
||||
// @ts-ignore
|
||||
value={notify_by}
|
||||
disabled={disabled}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
|
||||
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 { getUtilStyles } from 'styles/utils.styles';
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ import { Text } from 'components/Text/Text';
|
|||
import { TooltipBadge } from 'components/TooltipBadge/TooltipBadge';
|
||||
import { Schedule, ScheduleScoreQualityResult } from 'models/schedule/schedule.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import { getScheduleQualityStyles } from './ScheduleQuality.styles';
|
||||
|
||||
|
|
@ -50,7 +51,7 @@ export const ScheduleQuality: FC<ScheduleQualityProps> = observer(({ schedule })
|
|||
text={schedule.number_of_escalation_chains}
|
||||
tooltipTitle="Used in escalations"
|
||||
tooltipContent={
|
||||
<VerticalGroup spacing="sm">
|
||||
<Stack direction="column" gap={StackSize.sm}>
|
||||
{relatedScheduleEscalationChains.map((escalationChain) => (
|
||||
<div key={escalationChain.pk}>
|
||||
<PluginLink query={{ page: 'escalations', id: escalationChain.pk }} className="link">
|
||||
|
|
@ -58,7 +59,7 @@ export const ScheduleQuality: FC<ScheduleQualityProps> = observer(({ schedule })
|
|||
</PluginLink>
|
||||
</div>
|
||||
))}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -71,13 +72,13 @@ export const ScheduleQuality: FC<ScheduleQualityProps> = observer(({ schedule })
|
|||
text={schedule.warnings.length}
|
||||
tooltipTitle="Warnings"
|
||||
tooltipContent={
|
||||
<VerticalGroup spacing="none">
|
||||
<Stack direction="column" gap={StackSize.none}>
|
||||
{schedule.warnings.map((warning, index) => (
|
||||
<Text type="primary" key={index}>
|
||||
{warning}
|
||||
</Text>
|
||||
))}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React, { FC, useCallback, useState } from 'react';
|
||||
|
||||
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 { Text } from 'components/Text/Text';
|
||||
import { ScheduleScoreQualityResponse, ScheduleScoreQualityResult } from 'models/schedule/schedule.types';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import { getScheduleQualityDetailsStyles } from './ScheduleQualityDetails.styles';
|
||||
import { ScheduleQualityProgressBar } from './ScheduleQualityProgressBar';
|
||||
|
|
@ -112,19 +113,19 @@ export const ScheduleQualityDetails: FC<ScheduleQualityDetailsProps> = ({ qualit
|
|||
bem(styles.container, 'withLateralPadding')
|
||||
)}
|
||||
>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="sm">
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack gap={StackSize.sm}>
|
||||
<Icon name="calculator-alt" />
|
||||
<Text type="secondary" className={styles.metholodogy}>
|
||||
Calculation methodology
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
<IconButton
|
||||
aria-label={expanded ? 'Collapse' : 'Expand'}
|
||||
name={expanded ? 'arrow-down' : 'arrow-right'}
|
||||
onClick={handleExpandClick}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
{expanded && (
|
||||
<Text type="primary" className={styles.text}>
|
||||
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%')));
|
||||
});
|
||||
|
||||
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) => {
|
||||
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 { 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 { bem } from 'styles/utils.styles';
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ export const Text: TextInterface = (props) => {
|
|||
)}
|
||||
{isEditMode && (
|
||||
<Modal onDismiss={handleCancelEdit} closeOnEscape isOpen title={editModalTitle}>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Input
|
||||
autoFocus
|
||||
ref={(node) => {
|
||||
|
|
@ -149,15 +149,15 @@ export const Text: TextInterface = (props) => {
|
|||
value={value}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Stack justifyContent="flex-end">
|
||||
<Button variant="secondary" onClick={handleCancelEdit}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleConfirmEdit}>
|
||||
Ok
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
)}
|
||||
</CustomTag>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
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';
|
||||
|
||||
interface TimeRangeProps {
|
||||
|
|
@ -94,7 +94,7 @@ export const TimeRange = (props: TimeRangeProps) => {
|
|||
|
||||
return (
|
||||
<div className={cx(styles.root, className)}>
|
||||
<HorizontalGroup wrap>
|
||||
<Stack wrap="wrap">
|
||||
<div data-testid="time-range-from">
|
||||
{/* @ts-ignore actually TimeOfDayPicker uses Moment objects */}
|
||||
<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} />
|
||||
</div>
|
||||
{showNextDayTip && 'next day'}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
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 { Text, TextType } from 'components/Text/Text';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import { getTooltipBadgeStyles } from './TooltipBadge.styles';
|
||||
|
||||
|
|
@ -47,10 +48,10 @@ export const TooltipBadge: FC<TooltipBadgeProps> = (props) => {
|
|||
interactive
|
||||
content={
|
||||
<div className={styles.tooltip}>
|
||||
<VerticalGroup spacing="xs">
|
||||
<Stack direction="column" gap={StackSize.xs}>
|
||||
<Text type="primary">{tooltipTitle}</Text>
|
||||
{tooltipContent && <Text type="secondary">{tooltipContent}</Text>}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
@ -59,10 +60,10 @@ export const TooltipBadge: FC<TooltipBadgeProps> = (props) => {
|
|||
onMouseEnter={onHover}
|
||||
{...(testId ? { 'data-testid': testId } : {})}
|
||||
>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Stack gap={StackSize.xs}>
|
||||
{renderIcon()}
|
||||
{text !== undefined && <Text {...(testId ? { 'data-testid': `${testId}-text` } : {})}>{text}</Text>}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</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 { 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 { Text } from 'components/Text/Text';
|
||||
import { UserAction } from 'utils/authorization/authorization';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
type Props = {
|
||||
requiredUserAction: UserAction;
|
||||
|
|
@ -17,7 +18,7 @@ export const Unauthorized: FC<Props> = ({ requiredUserAction: { permission, fall
|
|||
|
||||
return (
|
||||
<div className={styles.notFound}>
|
||||
<VerticalGroup spacing="lg" align="center">
|
||||
<Stack direction="column" gap={StackSize.lg} alignItems="center">
|
||||
<Text.Title level={1} className={styles.errorCode}>
|
||||
403
|
||||
</Text.Title>
|
||||
|
|
@ -32,7 +33,7 @@ export const Unauthorized: FC<Props> = ({ requiredUserAction: { permission, fall
|
|||
<br />
|
||||
Please contact your organization administrator to request access.
|
||||
</Text.Title>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</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 { 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 { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
|
||||
import { bem } from 'styles/utils.styles';
|
||||
|
|
@ -99,7 +99,7 @@ export const UserGroups = (props: UserGroupsProps) => {
|
|||
{renderUser(item.data)}
|
||||
{!disabled && (
|
||||
<div className={styles.userButtons}>
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<IconButton
|
||||
aria-label="Remove"
|
||||
className={styles.icon}
|
||||
|
|
@ -107,7 +107,7 @@ export const UserGroups = (props: UserGroupsProps) => {
|
|||
onClick={getDeleteItemHandler(index)}
|
||||
/>
|
||||
<SortableHandleHoc />
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
|
|
@ -115,7 +115,7 @@ export const UserGroups = (props: UserGroupsProps) => {
|
|||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
{!disabled && (
|
||||
<RemoteSelect
|
||||
key={items.length}
|
||||
|
|
@ -142,7 +142,7 @@ export const UserGroups = (props: UserGroupsProps) => {
|
|||
useDragHandle
|
||||
allowCreate={!disabled}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { FC, useMemo } from 'react';
|
|||
|
||||
import { css } from '@emotion/css';
|
||||
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 { SourceCode } from 'components/SourceCode/SourceCode';
|
||||
|
|
@ -10,6 +10,7 @@ import { Tabs } from 'components/Tabs/Tabs';
|
|||
import { Text } from 'components/Text/Text';
|
||||
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import { WebhookStatusCodeBadge } from './WebhookStatusCodeBadge';
|
||||
|
||||
|
|
@ -41,14 +42,14 @@ export const WebhookLastEventDetails: FC<WebhookLastEventDetailsProps> = ({ webh
|
|||
return (
|
||||
<>
|
||||
<div className={styles.lastEventDetailsRowsWrapper}>
|
||||
<VerticalGroup spacing="md">
|
||||
<Stack direction="column" gap={StackSize.md}>
|
||||
{rows.map(({ title, value }) => (
|
||||
<HorizontalGroup key={title}>
|
||||
<Stack key={title}>
|
||||
<span className={styles.lastEventDetailsRowTitle}>{title}</span>
|
||||
<span className={styles.lastEventDetailsRowValue}>{value}</span>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
))}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
<Tabs
|
||||
queryStringKey="lastEventDetailsActiveTab"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
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 { Tag } from 'components/Tag/Tag';
|
||||
|
|
@ -43,7 +43,7 @@ export const WebhookLastEventTimestamp = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<Tag
|
||||
color={theme.colors.background.secondary}
|
||||
border={`1px solid ${theme.colors.border.weak}`}
|
||||
|
|
@ -61,7 +61,7 @@ export const WebhookLastEventTimestamp = ({
|
|||
className={styles.eventDetailsIconButton}
|
||||
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 { 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 { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ import { UserHelper } from 'models/user/user.helpers';
|
|||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import { getAddRespondersStyles } from './AddResponders.styles';
|
||||
import { NotificationPolicyValue, UserResponder as UserResponderType } from './AddResponders.types';
|
||||
|
|
@ -38,10 +39,10 @@ const LearnMoreAboutNotificationPoliciesLink: React.FC = () => {
|
|||
rel="noreferrer"
|
||||
>
|
||||
<Text type="link">
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Stack gap={StackSize.xs}>
|
||||
Learn more
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Text>
|
||||
</a>
|
||||
);
|
||||
|
|
@ -111,7 +112,7 @@ export const AddResponders = observer(
|
|||
<>
|
||||
<div className={styles.content}>
|
||||
<Block bordered>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Stack justifyContent="space-between">
|
||||
<Text.Title type="primary" level={4}>
|
||||
Participants
|
||||
</Text.Title>
|
||||
|
|
@ -128,7 +129,7 @@ export const AddResponders = observer(
|
|||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
{(selectedTeamResponder || existingPagedUsers.length > 0 || selectedUserResponders.length > 0) && (
|
||||
<>
|
||||
<ul className={styles.respondersList}>
|
||||
|
|
@ -189,7 +190,7 @@ export const AddResponders = observer(
|
|||
onDismiss={closeUserConfirmationModal}
|
||||
className={styles.confirmParticipantInvitationModal}
|
||||
>
|
||||
<VerticalGroup spacing="md">
|
||||
<Stack direction="column" gap={StackSize.md}>
|
||||
{!isCreateMode && (
|
||||
<div>
|
||||
<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."
|
||||
/>
|
||||
)}
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Stack justifyContent="flex-end">
|
||||
<Button variant="secondary" onClick={closeUserConfirmationModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={confirmCurrentlyConsideredUser} data-testid="confirm-non-oncall">
|
||||
Confirm
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</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 { 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 { observer } from 'mobx-react';
|
||||
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 { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { StackSize } from 'utils/consts';
|
||||
import { useDebouncedCallback, useOnClickOutside } from 'utils/hooks';
|
||||
|
||||
import styles from './AddRespondersPopup.module.scss';
|
||||
|
|
@ -205,17 +206,17 @@ export const AddRespondersPopup = observer(
|
|||
|
||||
return (
|
||||
<div onClick={() => addTeamResponder(team)} className={cx('responder-item')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack>
|
||||
<Avatar size="small" src={avatar_url} />
|
||||
<Text>{name}</Text>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
{number_of_users_currently_oncall > 0 && (
|
||||
<Text type="secondary">
|
||||
{number_of_users_currently_oncall} user{number_of_users_currently_oncall > 1 ? 's' : ''} on-call
|
||||
</Text>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
@ -233,20 +234,20 @@ export const AddRespondersPopup = observer(
|
|||
|
||||
return (
|
||||
<div onClick={() => (disabled ? undefined : onClickUser(user))} className={cx('responder-item')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack>
|
||||
<Avatar size="small" src={avatar} />
|
||||
<Text type={disabled ? 'disabled' : undefined} className={cx('responder-name')}>
|
||||
{name || username}
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
{/* TODO: we should add an elippsis and/or tooltip in the event that the user has a ton of teams */}
|
||||
{teams?.length > 0 && (
|
||||
<Text type="secondary" className={cx('responder-team')}>
|
||||
{teams.map(({ name }) => name).join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
@ -329,10 +330,10 @@ export const AddRespondersPopup = observer(
|
|||
rel="noreferrer"
|
||||
>
|
||||
<Text type="link">
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Stack gap={StackSize.xs}>
|
||||
Learn more
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Text>
|
||||
</a>
|
||||
</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',
|
||||
} 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 () => {
|
||||
const handleDelete = jest.fn();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { Text } from 'components/Text/Text';
|
||||
|
|
@ -17,20 +17,20 @@ export const TeamResponder: FC<Props> = ({ team: { avatar_url, name }, handleDel
|
|||
|
||||
return (
|
||||
<li>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack>
|
||||
<div className={styles.timelineIconBackground}>
|
||||
<Avatar size="medium" src={avatar_url} />
|
||||
</div>
|
||||
<Text className={styles.responderName}>{name}</Text>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
<IconButton
|
||||
data-testid="team-responder-delete-icon"
|
||||
tooltip="Remove responder"
|
||||
name="trash-alt"
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</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',
|
||||
} 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 () => {
|
||||
const handleDelete = jest.fn();
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { FC } from 'react';
|
|||
|
||||
import { cx } from '@emotion/css';
|
||||
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 { Text } from 'components/Text/Text';
|
||||
|
|
@ -27,14 +27,14 @@ export const UserResponder: FC<Props> = ({
|
|||
|
||||
return (
|
||||
<li>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack>
|
||||
<div className={cx(styles.timelineIconBackground, { 'timeline-icon-background--green': true })}>
|
||||
<Avatar size="medium" src={avatar} />
|
||||
</div>
|
||||
<Text className={styles.responderName}>{username}</Text>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<NotificationPoliciesSelect
|
||||
disabled={disableNotificationPolicySelect}
|
||||
important={important}
|
||||
|
|
@ -46,8 +46,8 @@ export const UserResponder: FC<Props> = ({
|
|||
name="trash-alt"
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</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 { VerticalGroup, useTheme2 } from '@grafana/ui';
|
||||
import { Stack, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { Timeline } from 'components/Timeline/Timeline';
|
||||
import { MSTeamsConnector } from 'containers/AlertRules/parts/connectors/MSTeamsConnector';
|
||||
|
|
@ -38,11 +38,11 @@ export const ChatOpsConnectors = (props: ChatOpsConnectorsProps) => {
|
|||
|
||||
return (
|
||||
<Timeline.Item number={0} backgroundHexNumber={theme.colors.secondary.main} isDisabled={!showLineNumber}>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
{isSlackInstalled && <SlackConnector channelFilterId={channelFilterId} />}
|
||||
{isTelegramInstalled && <TelegramConnector channelFilterId={channelFilterId} />}
|
||||
{isMSTeamsInstalled && <MSTeamsConnector channelFilterId={channelFilterId} />}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Timeline.Item>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { HorizontalGroup, InlineSwitch } from '@grafana/ui';
|
||||
import { InlineSwitch, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
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 { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import styles from 'containers/AlertRules/parts/connectors/Connectors.module.css';
|
||||
|
||||
|
|
@ -48,7 +49,7 @@ export const MSTeamsConnector = observer((props: MSTeamsConnectorProps) => {
|
|||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<HorizontalGroup wrap spacing="sm">
|
||||
<Stack wrap="wrap" gap={StackSize.sm}>
|
||||
<div className={cx('slack-channel-switch')}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<InlineSwitch
|
||||
|
|
@ -74,7 +75,7 @@ export const MSTeamsConnector = observer((props: MSTeamsConnectorProps) => {
|
|||
onChange={handleMSTeamsChannelChange}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { HorizontalGroup, InlineSwitch } from '@grafana/ui';
|
||||
import { InlineSwitch, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
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 { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import styles from './Connectors.module.css';
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ export const SlackConnector = observer((props: SlackConnectorProps) => {
|
|||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<HorizontalGroup wrap spacing="sm">
|
||||
<Stack wrap="wrap" gap={StackSize.sm}>
|
||||
<div className={cx('slack-channel-switch')}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<InlineSwitch
|
||||
|
|
@ -74,7 +75,7 @@ export const SlackConnector = observer((props: SlackConnectorProps) => {
|
|||
nullItemName={PRIVATE_CHANNEL_NAME}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { HorizontalGroup, InlineSwitch } from '@grafana/ui';
|
||||
import { InlineSwitch, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
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 { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import styles from './Connectors.module.css';
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ export const TelegramConnector = observer(({ channelFilterId }: TelegramConnecto
|
|||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<HorizontalGroup wrap spacing="sm">
|
||||
<Stack wrap="wrap" gap={StackSize.sm}>
|
||||
<div className={cx('slack-channel-switch')}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<InlineSwitch
|
||||
|
|
@ -66,7 +67,7 @@ export const TelegramConnector = observer(({ channelFilterId }: TelegramConnecto
|
|||
onChange={handleTelegramChannelChange}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { get } from 'lodash-es';
|
||||
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}>
|
||||
<FormProvider {...formMethods}>
|
||||
<form onSubmit={handleSubmit(onCreateTokenCallback)}>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Label>Token Name</Label>
|
||||
<div className={cx('token__inputContainer')}>
|
||||
{renderTokenInput()}
|
||||
|
|
@ -57,7 +57,7 @@ export const ApiTokenForm = observer((props: TokenCreationModalProps) => {
|
|||
|
||||
{renderCurlExample()}
|
||||
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Stack justifyContent="flex-end">
|
||||
<Button variant="secondary" onClick={() => onHide()}>
|
||||
{token ? 'Close' : 'Cancel'}
|
||||
</Button>
|
||||
|
|
@ -67,8 +67,8 @@ export const ApiTokenForm = observer((props: TokenCreationModalProps) => {
|
|||
Create Token
|
||||
</Button>
|
||||
</RenderConditionally>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</Modal>
|
||||
|
|
@ -117,12 +117,12 @@ export const ApiTokenForm = observer((props: TokenCreationModalProps) => {
|
|||
return null;
|
||||
}
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Label>Curl command example</Label>
|
||||
<SourceCode noMinHeight showClipboardIconOnly>
|
||||
{getCurlExample(token, store.pluginStore.apiUrlFromStatus)}
|
||||
</SourceCode>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, HorizontalGroup } from '@grafana/ui';
|
||||
import { Button, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import moment from 'moment-timezone';
|
||||
|
|
@ -82,9 +82,9 @@ class _ApiTokenSettings extends React.Component<ApiTokensProps, any> {
|
|||
<GTable
|
||||
title={() => (
|
||||
<div className={cx('header')}>
|
||||
<HorizontalGroup align="flex-end">
|
||||
<Stack alignItems="flex-end">
|
||||
<Text.Title level={3}>API Tokens</Text.Title>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
<WithPermissionControlTooltip userAction={UserActions.APIKeysWrite}>
|
||||
<Button
|
||||
icon="plus"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
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 { observer } from 'mobx-react';
|
||||
import moment from 'moment-timezone';
|
||||
|
|
@ -65,10 +65,10 @@ export const AttachIncidentForm = observer(({ id, onUpdate, onHide }: AttachInci
|
|||
isOpen
|
||||
icon="link"
|
||||
title={
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<Icon size="lg" name="link" />
|
||||
<Text.Title level={4}>Attach to another alert group</Text.Title>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
}
|
||||
className={cx('root')}
|
||||
onDismiss={onHide}
|
||||
|
|
@ -97,14 +97,14 @@ export const AttachIncidentForm = observer(({ id, onUpdate, onHide }: AttachInci
|
|||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</Field>
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<Button onClick={onHide} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleLinkClick} variant="primary" disabled={!selected}>
|
||||
Attach
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,17 +1,7 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { LabelTag } from '@grafana/labels';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
HorizontalGroup,
|
||||
IconButton,
|
||||
Input,
|
||||
LoadingPlaceholder,
|
||||
Modal,
|
||||
VerticalGroup,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { Button, Checkbox, IconButton, Input, LoadingPlaceholder, Modal, Stack, useStyles2 } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
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 { useStore } from 'state/useStore';
|
||||
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 { useDebouncedCallback, useIsLoading } from 'utils/hooks';
|
||||
import { pluralize } from 'utils/utils';
|
||||
|
|
@ -68,9 +58,9 @@ export const ColumnsModal: React.FC<ColumnsModalProps> = observer(
|
|||
|
||||
return (
|
||||
<Modal isOpen={isModalOpen} title={'Add column'} onDismiss={onCloseModal} closeOnEscape={false}>
|
||||
<VerticalGroup spacing="md">
|
||||
<Stack direction="column" gap={StackSize.md}>
|
||||
<div className={styles.content}>
|
||||
<VerticalGroup spacing="md">
|
||||
<Stack direction="column" gap={StackSize.md}>
|
||||
<Input
|
||||
className={styles.input}
|
||||
autoFocus
|
||||
|
|
@ -87,9 +77,9 @@ export const ColumnsModal: React.FC<ColumnsModalProps> = observer(
|
|||
)}
|
||||
|
||||
{inputRef?.current?.value && searchResults.length && (
|
||||
<VerticalGroup spacing="none">
|
||||
<Stack direction="column" gap={StackSize.none}>
|
||||
{searchResults.map((result, index) => (
|
||||
<VerticalGroup key={index}>
|
||||
<Stack direction="column" key={index}>
|
||||
<div className={styles.fieldRow}>
|
||||
<IconButton
|
||||
aria-label={result.isCollapsed ? 'Expand' : 'Collapse'}
|
||||
|
|
@ -122,18 +112,18 @@ export const ColumnsModal: React.FC<ColumnsModalProps> = observer(
|
|||
)}
|
||||
</Block>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
))}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{inputRef?.current?.value && searchResults.length === 0 && (
|
||||
<Text type="primary">0 results for your search.</Text>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<HorizontalGroup justify="flex-end" spacing="md">
|
||||
<Stack justifyContent="flex-end" gap={StackSize.md}>
|
||||
<Button variant="secondary" onClick={onCloseModal}>
|
||||
Close
|
||||
</Button>
|
||||
|
|
@ -149,19 +139,19 @@ export const ColumnsModal: React.FC<ColumnsModalProps> = observer(
|
|||
{isLoading ? <LoadingPlaceholder className={cx('loadingPlaceholder')} text="Loading..." /> : 'Add'}
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
function renderLabelValues(keyName: string, values: Array<ApiSchemas['LabelValue']>) {
|
||||
return (
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Stack gap={StackSize.xs}>
|
||||
{values.slice(0, 2).map((val) => (
|
||||
<LabelTag label={keyName} value={val.name} key={val.id} />
|
||||
))}
|
||||
<div>{values.length > 2 ? `+ ${values.length - 2}` : ``}</div>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { 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 { useStore } from 'state/useStore';
|
||||
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 { useIsLoading } from 'utils/hooks';
|
||||
|
||||
|
|
@ -68,10 +68,10 @@ export const ColumnsSelectorWrapper: React.FC<ColumnsSelectorWrapperProps> = obs
|
|||
onDismiss={onConfirmRemovalClose}
|
||||
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>
|
||||
|
||||
<HorizontalGroup justify="flex-end" spacing="md">
|
||||
<Stack justifyContent="flex-end" gap={StackSize.md}>
|
||||
<Button variant={'secondary'} onClick={onConfirmRemovalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
@ -90,8 +90,8 @@ export const ColumnsSelectorWrapper: React.FC<ColumnsSelectorWrapperProps> = obs
|
|||
{isRemoveLoading ? <LoadingPlaceholder text="Loading..." className="loadingPlaceholder" /> : 'Remove'}
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
<div ref={wrappingFloatingContainerRef}>
|
||||
|
|
@ -147,10 +147,10 @@ export const ColumnsSelectorWrapper: React.FC<ColumnsSelectorWrapperProps> = obs
|
|||
id="toggletip-button"
|
||||
onClick={() => setIsFloatingDisplayOpen(!isFloatingDisplayOpen)}
|
||||
>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Stack gap={StackSize.xs}>
|
||||
Columns
|
||||
<Icon name="angle-down" />
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { debounce } from 'lodash-es';
|
||||
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 { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { StackSize } from 'utils/consts';
|
||||
import { openErrorNotification } from 'utils/utils';
|
||||
|
||||
import styles from './EditRegexpRouteTemplateModal.module.css';
|
||||
|
|
@ -78,9 +79,9 @@ export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemp
|
|||
title="Edit regular expression template"
|
||||
className={cx('regexp-template-editor-modal')}
|
||||
>
|
||||
<VerticalGroup spacing="lg">
|
||||
<VerticalGroup spacing="xs">
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Stack direction="column" gap={StackSize.lg}>
|
||||
<Stack direction="column" gap={StackSize.xs}>
|
||||
<Stack gap={StackSize.xs}>
|
||||
<Text type={'secondary'}>Regular expression</Text>
|
||||
<Tooltip
|
||||
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'} />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
|
||||
<div className={cx('regexp-template-code', { 'regexp-template-code-error': showErrorTemplate })}>
|
||||
<MonacoEditor
|
||||
|
|
@ -99,16 +100,16 @@ export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemp
|
|||
onChange={handleRegexpBodyChange()}
|
||||
/>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
<VerticalGroup>
|
||||
</Stack>
|
||||
<Stack direction="column">
|
||||
<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>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
<Block bordered fullWidth withBackground>
|
||||
<Text type="link">{templateJinja2Body}</Text>
|
||||
</Block>
|
||||
|
||||
<HorizontalGroup justify={'flex-end'}>
|
||||
<Stack justifyContent={'flex-end'}>
|
||||
<Button variant={'secondary'} onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
@ -118,8 +119,8 @@ export const EditRegexpRouteTemplateModal = observer((props: EditRegexpRouteTemp
|
|||
<Button variant={'primary'} onClick={() => handleSave()}>
|
||||
Save
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { HorizontalGroup, VerticalGroup, Badge } from '@grafana/ui';
|
||||
import { Stack, Badge } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -8,6 +8,7 @@ import { Text } from 'components/Text/Text';
|
|||
import { TeamName } from 'containers/TeamName/TeamName';
|
||||
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import styles from './EscalationChainCard.module.css';
|
||||
|
||||
|
|
@ -28,12 +29,13 @@ export const EscalationChainCard = observer((props: AlertReceiveChannelCardProps
|
|||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<VerticalGroup spacing="xs">
|
||||
<HorizontalGroup spacing="sm">
|
||||
<Stack alignItems="flex-start">
|
||||
<Stack direction="column" gap={StackSize.xs}>
|
||||
<Stack gap={StackSize.sm}>
|
||||
<Text type="primary" size="medium">
|
||||
{escalationChain.name}
|
||||
</Text>
|
||||
<div>
|
||||
<Badge
|
||||
text={escalationChain.number_of_integrations}
|
||||
color="green"
|
||||
|
|
@ -44,10 +46,14 @@ export const EscalationChainCard = observer((props: AlertReceiveChannelCardProps
|
|||
: 'This escalation is not connected to any integration route, go to integrations and connect route to this escalation chain'
|
||||
}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<div>
|
||||
<TeamName team={grafanaTeamStore.items[escalationChain.team]} size="small" />
|
||||
</VerticalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { observer } from 'mobx-react';
|
||||
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}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="primary">
|
||||
{`${mode} Escalation Chain`}
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ interface GSelectProps<Item> {
|
|||
openMenuOnFocus?: boolean;
|
||||
width?: number | 'auto';
|
||||
icon?: string;
|
||||
dataTestId?: string;
|
||||
}
|
||||
|
||||
export const GSelect = observer(<Item,>(props: GSelectProps<Item>) => {
|
||||
|
|
@ -72,6 +73,7 @@ export const GSelect = observer(<Item,>(props: GSelectProps<Item>) => {
|
|||
fetchItemFn,
|
||||
getSearchResult,
|
||||
parseDisplayName,
|
||||
dataTestId = null,
|
||||
} = props;
|
||||
|
||||
const onChangeCallback = useCallback(
|
||||
|
|
@ -151,7 +153,7 @@ export const GSelect = observer(<Item,>(props: GSelectProps<Item>) => {
|
|||
const Tag = isMulti ? AsyncMultiSelect : AsyncSelect;
|
||||
|
||||
return (
|
||||
<div className={cx('root', className)}>
|
||||
<div className={cx('root', className)} data-testid={dataTestId}>
|
||||
<Tag
|
||||
autoFocus={autoFocus}
|
||||
isSearchable
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ export const GrafanaTeamSelect = observer(
|
|||
|
||||
return (
|
||||
<Modal onDismiss={onHide} closeOnEscape isOpen title="Select team" className={cx('root')}>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Label>
|
||||
<span className={cx('teamSelectText')}>
|
||||
Select team{''}
|
||||
|
|
@ -92,12 +92,12 @@ export const GrafanaTeamSelect = observer(
|
|||
Edit teams
|
||||
</a>
|
||||
</WithPermissionControlTooltip>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Stack justifyContent="flex-end">
|
||||
<Button variant="primary" onClick={handleConfirm}>
|
||||
Ok
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -15,6 +15,7 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
|
|||
import { CommonIntegrationHelper } from 'pages/integration/CommonIntegration.helper';
|
||||
import { IntegrationHelper } from 'pages/integration/Integration.helper';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
|
|
@ -95,7 +96,7 @@ export const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRout
|
|||
<div className={cx('collapsedRoute__container')}>
|
||||
{chatOpsAvailableChannels.length > 0 && (
|
||||
<div className={cx('collapsedRoute__item')}>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Stack gap={StackSize.xs}>
|
||||
<Text type="secondary">Publish to ChatOps</Text>
|
||||
|
||||
{chatOpsAvailableChannels.map(
|
||||
|
|
@ -109,7 +110,7 @@ export const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRout
|
|||
</div>
|
||||
)
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import React, { useEffect, useReducer, useState } from 'react';
|
|||
import { SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
Button,
|
||||
HorizontalGroup,
|
||||
VerticalGroup,
|
||||
Stack,
|
||||
Icon,
|
||||
Tooltip,
|
||||
ConfirmModal,
|
||||
|
|
@ -44,6 +43,7 @@ import { MONACO_INPUT_HEIGHT_SMALL } from 'pages/integration/IntegrationCommon.c
|
|||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { StackSize } from 'utils/consts';
|
||||
import { openNotification } from 'utils/utils';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
|
@ -179,13 +179,13 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
|||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
<VerticalGroup spacing="sm">
|
||||
<Stack direction="column" gap={StackSize.sm}>
|
||||
<Text customTag="h6" type="primary">
|
||||
{hasLabels ? 'Alerts matched by' : 'Use routing template'}
|
||||
</Text>
|
||||
|
||||
<RenderConditionally shouldRender={hasLabels}>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<div className={cx('labels-panel')}>
|
||||
<RadioButtonGroup
|
||||
options={QueryBuilderOptions}
|
||||
|
|
@ -195,7 +195,7 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
|||
</div>
|
||||
|
||||
<RenderConditionally shouldRender={routingOption === RoutingOption.LABELS}>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<RouteLabelsDisplay labels={labels} onChange={onLabelsChange} labelErrors={labelErrors} />
|
||||
|
||||
<RenderConditionally shouldRender={shouldShowLabelAlert()}>
|
||||
|
|
@ -210,14 +210,14 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
|||
}
|
||||
/>
|
||||
</RenderConditionally>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</RenderConditionally>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</RenderConditionally>
|
||||
|
||||
<RenderConditionally shouldRender={routingOption === RoutingOption.TEMPLATE || !hasLabels}>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Stack direction="column">
|
||||
<Stack gap={StackSize.xs}>
|
||||
<div className={cx('input', 'input--align')}>
|
||||
<MonacoEditor
|
||||
value={channelFilterTemplate}
|
||||
|
|
@ -234,7 +234,7 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
|||
size={'md'}
|
||||
onClick={() => handleEditRoutingTemplate(channelFilter, channelFilterId)}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
<Alert
|
||||
severity="info"
|
||||
title={
|
||||
|
|
@ -246,9 +246,9 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
|||
) as unknown as string
|
||||
}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</RenderConditionally>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
|
|
@ -261,12 +261,12 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
|||
canHoverIcon: false,
|
||||
expandedView: () => (
|
||||
<div className={cx('adjust-element-padding')}>
|
||||
<VerticalGroup spacing="sm">
|
||||
<Stack direction="column" gap={StackSize.sm}>
|
||||
<Text customTag="h6" type="primary">
|
||||
Publish to ChatOps
|
||||
</Text>
|
||||
<ChatOpsConnectors channelFilterId={channelFilterId} showLineNumber={false} />
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
@ -279,13 +279,13 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
|||
canHoverIcon: false,
|
||||
expandedView: () => (
|
||||
<div className={cx('adjust-element-padding')}>
|
||||
<VerticalGroup spacing="sm">
|
||||
<Stack direction="column" gap={StackSize.sm}>
|
||||
<Text customTag="h6" type="primary">
|
||||
Trigger escalation chain
|
||||
</Text>
|
||||
|
||||
<div data-testid="escalation-chain-select">
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Stack gap={StackSize.xs}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Select
|
||||
isClearable
|
||||
|
|
@ -336,19 +336,19 @@ export const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteD
|
|||
variant={'secondary'}
|
||||
onClick={() => setState({ isEscalationCollapsed: !isEscalationCollapsed })}
|
||||
>
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<Text type="link">{isEscalationCollapsed ? 'Show' : 'Hide'} escalation chain</Text>
|
||||
{isEscalationCollapsed && <Icon name={'angle-right'} />}
|
||||
{!isEscalationCollapsed && <Icon name={'angle-up'} />}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Button>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
{!isEscalationCollapsed && (
|
||||
<ReadOnlyEscalationChain escalationChainId={channelFilter.escalation_chain} />
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
@ -506,7 +506,7 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
|||
const channelFilterIds = alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId];
|
||||
|
||||
return (
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Stack gap={StackSize.xs}>
|
||||
{routeIndex > 0 && !channelFilter.is_default && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<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')}>
|
||||
<div className={cx('integrations-actionItem')}>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Stack gap={StackSize.xs}>
|
||||
<Icon name="copy" />
|
||||
|
||||
<Text type="primary">UID: {channelFilter.id}</Text>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
|
||||
|
|
@ -546,10 +546,10 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
|||
<WithPermissionControlTooltip key="delete" userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integrations-actionItem')} onClick={onDelete}>
|
||||
<Text type="danger">
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Stack gap={StackSize.xs}>
|
||||
<Icon name="trash-alt" />
|
||||
<span>Delete Route</span>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Text>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
|
|
@ -567,7 +567,7 @@ export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
|||
)}
|
||||
</WithContextMenu>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
function onDelete() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
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 { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ import { SelectOption } from 'state/types';
|
|||
import { useStore } from 'state/useStore';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { StackSize } from 'utils/consts';
|
||||
import { openNotification } from 'utils/utils';
|
||||
|
||||
import styles from './IntegrationHeartbeatForm.module.scss';
|
||||
|
|
@ -47,14 +48,14 @@ const _IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: I
|
|||
return (
|
||||
<Drawer width={'640px'} scrollableContent title={'Heartbeat'} onClose={onClose} closeOnMaskClick={false}>
|
||||
<div data-testid="heartbeat-settings-form">
|
||||
<VerticalGroup spacing={'lg'}>
|
||||
<Stack direction="column" gap={StackSize.lg}>
|
||||
<Text type="secondary">
|
||||
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
|
||||
alert group and escalate it
|
||||
</Text>
|
||||
|
||||
<VerticalGroup spacing="md">
|
||||
<Stack direction="column" gap={StackSize.md}>
|
||||
<div className={cx('u-width-100')}>
|
||||
<Field label={'Setup heartbeat interval'}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
|
|
@ -83,16 +84,17 @@ const _IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: I
|
|||
rel="noreferrer"
|
||||
>
|
||||
<Text type="link" size="small">
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
How to configure heartbeats
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Text>
|
||||
</a>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
|
||||
<VerticalGroup style={{ marginTop: 'auto' }}>
|
||||
<HorizontalGroup className={cx('buttons')} justify="flex-end">
|
||||
{/* TODO: Check if the styles were appended previously */}
|
||||
<Stack direction="column">
|
||||
<Stack justifyContent="flex-end">
|
||||
<Button variant={'secondary'} onClick={onClose} data-testid="close-heartbeat-form">
|
||||
Close
|
||||
</Button>
|
||||
|
|
@ -108,9 +110,9 @@ const _IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: I
|
|||
</Button>
|
||||
</WithConfirm>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { SelectableValue } from '@grafana/data';
|
|||
import {
|
||||
Button,
|
||||
Field,
|
||||
HorizontalGroup,
|
||||
Icon,
|
||||
Input,
|
||||
Label,
|
||||
|
|
@ -14,7 +13,7 @@ import {
|
|||
Switch,
|
||||
TextArea,
|
||||
Tooltip,
|
||||
VerticalGroup,
|
||||
Stack,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -37,7 +36,13 @@ import { IntegrationHelper, getIsBidirectionalIntegration } from 'pages/integrat
|
|||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
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 { validateURL } from 'utils/string';
|
||||
import { OmitReadonlyMembers } from 'utils/types';
|
||||
|
|
@ -296,17 +301,17 @@ export const IntegrationForm = observer(
|
|||
|
||||
<RenderConditionally shouldRender={isServiceNow && isNew}>
|
||||
<div className={styles.serviceNowHeading}>
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<Text type="primary">ServiceNow configuration</Text>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text type={'primary'} size={'small'}>
|
||||
Fill in ServiceNow credentials to be used by Grafana OnCall.{' '}
|
||||
<a href={`${DOCS_ROOT}/integrations/servicenow/`} target="_blank" rel="noreferrer">
|
||||
<Text type="link">Read setup guide</Text>
|
||||
</a>
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
|
|
@ -382,7 +387,7 @@ export const IntegrationForm = observer(
|
|||
</RenderConditionally>
|
||||
|
||||
<div>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Stack justifyContent="flex-end">
|
||||
{id === 'new' ? (
|
||||
<Button variant="secondary" onClick={onBackClick}>
|
||||
Back
|
||||
|
|
@ -396,7 +401,7 @@ export const IntegrationForm = observer(
|
|||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
{renderUpdateIntegrationButton(id)}
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
|
@ -543,13 +548,13 @@ const GrafanaContactPoint = observer(
|
|||
|
||||
return (
|
||||
<div className={styles.extraFields}>
|
||||
<VerticalGroup spacing="md">
|
||||
<HorizontalGroup spacing="xs" align="center">
|
||||
<Stack direction="column" gap={StackSize.md}>
|
||||
<Stack gap={StackSize.xs} alignItems="center">
|
||||
<Text type="primary" size="small">
|
||||
Grafana Alerting Contact point
|
||||
</Text>
|
||||
<Icon name="info-circle" />
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
|
||||
<div className={styles.extraFieldsRadio}>
|
||||
<Controller
|
||||
|
|
@ -625,7 +630,7 @@ const GrafanaContactPoint = observer(
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ import { IntegrationLogo } from 'components/IntegrationLogo/IntegrationLogo';
|
|||
import { Text } from 'components/Text/Text';
|
||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import { IntegrationForm } from './IntegrationForm';
|
||||
import styles from './IntegrationFormContainer.module.scss';
|
||||
|
|
@ -59,7 +60,7 @@ export const IntegrationFormContainer = observer((props: IntegrationFormContaine
|
|||
{showIntegrationsListDrawer && (
|
||||
<Drawer scrollableContent title="New Integration" onClose={onHide} closeOnMaskClick={false} width="640px">
|
||||
<div className={cx('content')}>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Text type="secondary">
|
||||
Integration receives alerts on an unique API URL, interprets them using set of templates tailored for
|
||||
monitoring system and starts escalations.
|
||||
|
|
@ -75,14 +76,14 @@ export const IntegrationFormContainer = observer((props: IntegrationFormContaine
|
|||
</div>
|
||||
|
||||
<IntegrationBlocks options={options} onBlockClick={onBlockClick} />
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</Drawer>
|
||||
)}
|
||||
{(showNewIntegrationForm || !showIntegrationsListDrawer) && (
|
||||
<Drawer scrollableContent title={getTitle()} onClose={onHide} closeOnMaskClick={false} width="640px">
|
||||
<div className={cx('content')}>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<IntegrationForm
|
||||
id={id}
|
||||
onBackClick={onBackClick}
|
||||
|
|
@ -91,7 +92,7 @@ export const IntegrationFormContainer = observer((props: IntegrationFormContaine
|
|||
onSubmit={onSubmit}
|
||||
onHide={onHide}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</Drawer>
|
||||
)}
|
||||
|
|
@ -139,19 +140,19 @@ const IntegrationBlocks: React.FC<{
|
|||
<IntegrationLogo integration={alertReceiveChannelChoice} scale={0.2} />
|
||||
</div>
|
||||
<div className={cx('title')}>
|
||||
<VerticalGroup spacing={alertReceiveChannelChoice.featured ? 'xs' : 'none'}>
|
||||
<HorizontalGroup>
|
||||
<Stack direction="column" gap={alertReceiveChannelChoice.featured ? StackSize.xs : StackSize.none}>
|
||||
<Stack>
|
||||
<Text strong data-testid="integration-display-name">
|
||||
{alertReceiveChannelChoice.display_name}
|
||||
</Text>
|
||||
{alertReceiveChannelChoice.featured && alertReceiveChannelChoice.featured_tag_name && (
|
||||
<Tag name={alertReceiveChannelChoice.featured_tag_name} colorIndex={5} />
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
<Text type="secondary" size="small">
|
||||
{alertReceiveChannelChoice.short_description}
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</Block>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,7 @@
|
|||
import React, { ChangeEvent, useState } from 'react';
|
||||
|
||||
import { ServiceLabels } from '@grafana/labels';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Drawer,
|
||||
Dropdown,
|
||||
HorizontalGroup,
|
||||
InlineSwitch,
|
||||
Input,
|
||||
Menu,
|
||||
VerticalGroup,
|
||||
} from '@grafana/ui';
|
||||
import { Alert, Button, Drawer, Dropdown, InlineSwitch, Input, Menu, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
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 { LabelTemplateOptions } from 'pages/integration/IntegrationCommon.config';
|
||||
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 { getIsAddBtnDisabled, getIsTooManyLabelsWarningVisible } from './IntegrationLabelsForm.helpers';
|
||||
|
|
@ -105,7 +95,7 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
|||
closeOnMaskClick={false}
|
||||
width="640px"
|
||||
>
|
||||
<VerticalGroup spacing="lg">
|
||||
<Stack direction="column" gap={StackSize.lg}>
|
||||
<RenderConditionally shouldRender={getIsTooManyLabelsWarningVisible(alertGroupLabels)}>
|
||||
<Alert title="More than 15 labels added" severity="warning">
|
||||
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.
|
||||
</Alert>
|
||||
</RenderConditionally>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Text>Integration labels</Text>
|
||||
{alertReceiveChannel.labels.length ? (
|
||||
<VerticalGroup spacing="xs">
|
||||
<Stack direction="column" gap={StackSize.xs}>
|
||||
<Text type="secondary" size="small">
|
||||
Labels inherited from <PluginLink onClick={handleOpenIntegrationSettings}>the integration</PluginLink>
|
||||
. This behavior can be disabled using the toggle option.
|
||||
|
|
@ -124,7 +114,7 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
|||
<ul className={cx('labels-list')}>
|
||||
{alertReceiveChannel.labels.map((label) => (
|
||||
<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.value.name} disabled />
|
||||
<InlineSwitch
|
||||
|
|
@ -132,20 +122,20 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
|||
transparent
|
||||
onChange={() => onInheritanceChange(label.key.id)}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
) : (
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Text type="secondary">There are no labels to inherit yet</Text>
|
||||
<Text type="link" onClick={handleOpenIntegrationSettings} clickable>
|
||||
Add labels to the integration
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
|
||||
<CustomLabels
|
||||
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">
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup justify="space-between" style={{ marginBottom: '10px' }} align="flex-end">
|
||||
<Stack direction="column">
|
||||
<Stack justifyContent="space-between" alignItems="flex-end">
|
||||
<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
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
<MonacoEditor
|
||||
value={alertGroupLabels.template}
|
||||
height="200px"
|
||||
|
|
@ -183,20 +173,20 @@ export const IntegrationLabelsForm = observer((props: IntegrationLabelsFormProps
|
|||
setAlertGroupLabels({ ...alertGroupLabels, template: value });
|
||||
}}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
|
||||
<div className={cx('buttons')}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Stack justifyContent="flex-end">
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Drawer>
|
||||
{customLabelIndexToShowTemplateEditor !== undefined && (
|
||||
<IntegrationTemplate
|
||||
|
|
@ -297,7 +287,7 @@ const CustomLabels = (props: CustomLabelsProps) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Text>Dynamic & Static labels</Text>
|
||||
<Text type="secondary" size="small">
|
||||
Dynamic: label values are extracted from the alert payload using Jinja. Keys remain static.
|
||||
|
|
@ -376,6 +366,6 @@ const CustomLabels = (props: CustomLabelsProps) => {
|
|||
Add label
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -165,13 +165,13 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
|
|||
<Drawer
|
||||
title={
|
||||
<div className={cx('title-container')}>
|
||||
<HorizontalGroup justify="space-between" align="flex-start">
|
||||
<VerticalGroup>
|
||||
<Stack justifyContent="space-between" alignItems="flex-start">
|
||||
<Stack direction="column">
|
||||
<Text.Title level={3}>Edit {template.displayName} template</Text.Title>
|
||||
{template.description && <Text type="secondary">{template.description}</Text>}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
|
|
@ -182,8 +182,8 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
|
|||
Save
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
}
|
||||
onClose={onHide}
|
||||
|
|
@ -231,13 +231,13 @@ export const IntegrationTemplate = observer((props: IntegrationTemplateProps) =>
|
|||
<>
|
||||
<div className={cx('template-block-codeeditor')}>
|
||||
<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>
|
||||
|
||||
<Button variant="secondary" fill="outline" onClick={onShowCheatSheet} icon="book" size="sm">
|
||||
Cheatsheet
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
<div className={cx('template-editor-block-content')}>
|
||||
<MonacoEditor
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { observer } from 'mobx-react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
|
|
@ -10,6 +10,7 @@ import { PluginLink } from 'components/PluginLink/PluginLink';
|
|||
import { Text } from 'components/Text/Text';
|
||||
import MSTeamsLogo from 'icons/MSTeamsLogo';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { StackSize } from 'utils/consts';
|
||||
import { openNotification, openWarningNotification } from 'utils/utils';
|
||||
|
||||
import styles from './MSTeamsInstructions.module.css';
|
||||
|
|
@ -40,36 +41,36 @@ export const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props
|
|||
};
|
||||
|
||||
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>}
|
||||
{showInfoBox && (
|
||||
<Block bordered withBackground className={cx('info-block')}>
|
||||
<VerticalGroup align="center">
|
||||
<Stack direction="column" alignItems="center">
|
||||
<div style={{ width: '60px', marginTop: '24px' }}>
|
||||
<MSTeamsLogo />
|
||||
</div>
|
||||
<Text>You can manage alert groups in your Microsoft Teams workspace.</Text>
|
||||
<br />
|
||||
{personalSettings ? (
|
||||
<VerticalGroup align="center">
|
||||
<Stack direction="column" alignItems="center">
|
||||
<Text>This setup is for direct profile connection with bot. </Text>
|
||||
<br />
|
||||
<Text className={cx('infoblock-text')}>
|
||||
To manage alert groups in Team channel, setup{' '}
|
||||
<PluginLink query={{ page: 'chat-ops', tab: 'MSTeams' }}>Team ChatOps</PluginLink>
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
) : (
|
||||
<VerticalGroup align="center">
|
||||
<Stack direction="column" alignItems="center">
|
||||
<Text>This setup is for Team channel connection with bot. </Text>
|
||||
<br />
|
||||
<Text className={cx('infoblock-text')}>
|
||||
To manage alert groups in Direct Messages and verify users who are allowed to operate with MS Teams,
|
||||
setup <PluginLink query={{ page: 'users', id: 'me' }}>personal MS Teams connection</PluginLink>
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
|
|
@ -126,6 +127,6 @@ export const MSTeamsInstructions: FC<MSTeamsInstructionsProps> = observer((props
|
|||
<Button onClick={handleMSTeamsGetChannels}>Done</Button>
|
||||
</div>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
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 { observer } from 'mobx-react';
|
||||
import Emoji from 'react-emoji-render';
|
||||
|
|
@ -74,7 +74,7 @@ export const MaintenanceForm = observer((props: MaintenanceFormProps) => {
|
|||
return (
|
||||
<Drawer width="640px" scrollableContent title="Start Maintenance Mode" onClose={onHide} closeOnMaskClick={false}>
|
||||
<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
|
||||
trigger false alarms.
|
||||
<FormProvider {...formMethods}>
|
||||
|
|
@ -182,7 +182,7 @@ export const MaintenanceForm = observer((props: MaintenanceFormProps) => {
|
|||
</Field>
|
||||
)}
|
||||
/>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Stack justifyContent="flex-end">
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
@ -191,10 +191,10 @@ export const MaintenanceForm = observer((props: MaintenanceFormProps) => {
|
|||
Start
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MemoryRouter } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { UserHelper } from 'models/user/user.helpers';
|
||||
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 () => {
|
||||
const component = render(<MobileAppConnection userPk={USER_PK} />);
|
||||
expect(component.container).toMatchSnapshot();
|
||||
render(<MobileAppConnection userPk={USER_PK} />);
|
||||
|
||||
await waitFor(() => {
|
||||
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 () => {
|
||||
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 waitFor(() => {
|
||||
expect(component.container).toMatchSnapshot();
|
||||
|
||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledWith(USER_PK, BACKEND);
|
||||
});
|
||||
});
|
||||
|
||||
test("it shows a QR code if the app isn't already connected", async () => {
|
||||
const component = render(<MobileAppConnection userPk={USER_PK} />);
|
||||
expect(component.container).toMatchSnapshot();
|
||||
render(<MobileAppConnection userPk={USER_PK} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
||||
|
|
@ -113,8 +108,6 @@ describe('MobileAppConnection', () => {
|
|||
// click the confirm button within the modal, which actually triggers the callback
|
||||
await userEvent.click(screen.getByText('Remove'));
|
||||
|
||||
// expect(component.container).toMatchSnapshot();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledWith(USER_PK, BACKEND);
|
||||
|
|
@ -132,7 +125,7 @@ describe('MobileAppConnection', () => {
|
|||
true
|
||||
);
|
||||
|
||||
const component = render(<MobileAppConnection userPk={USER_PK} />);
|
||||
render(<MobileAppConnection userPk={USER_PK} />);
|
||||
const button = await screen.findByRole('button');
|
||||
|
||||
// click the disconnect button, which opens the modal
|
||||
|
|
@ -143,8 +136,6 @@ describe('MobileAppConnection', () => {
|
|||
// wait for loading state
|
||||
await screen.findByText(/.*Loading.*/);
|
||||
|
||||
expect(component.container).toMatchSnapshot();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(1);
|
||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledWith(USER_PK, BACKEND);
|
||||
|
|
@ -162,7 +153,7 @@ describe('MobileAppConnection', () => {
|
|||
true
|
||||
);
|
||||
|
||||
const component = render(<MobileAppConnection userPk={USER_PK} />);
|
||||
render(<MobileAppConnection userPk={USER_PK} />);
|
||||
const button = await screen.findByTestId('test__disconnect');
|
||||
|
||||
// click the disconnect button, which opens the modal
|
||||
|
|
@ -172,8 +163,6 @@ describe('MobileAppConnection', () => {
|
|||
|
||||
await screen.findByText(/.*error disconnecting your mobile app.*/);
|
||||
|
||||
expect(component.container).toMatchSnapshot();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(UserHelper.fetchBackendConfirmationCode).toHaveBeenCalledTimes(0);
|
||||
|
||||
|
|
@ -223,16 +212,4 @@ describe('MobileAppConnection', () => {
|
|||
{ 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 { Button, HorizontalGroup, Icon, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import { Button, Icon, LoadingPlaceholder, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -16,6 +16,7 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
|
|||
import { AppFeature } from 'state/features';
|
||||
import { RootStore, rootStore as store } from 'state/rootStore';
|
||||
import { UserActions } from 'utils/authorization/authorization';
|
||||
import { StackSize } from 'utils/consts';
|
||||
import { useInitializePlugin } from 'utils/hooks';
|
||||
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>;
|
||||
} else if (mobileAppIsCurrentlyConnected) {
|
||||
content = (
|
||||
<VerticalGroup spacing="lg">
|
||||
<Stack direction="column" gap={StackSize.lg}>
|
||||
<Text strong type="primary">
|
||||
App connected <Icon name="check-circle" size="md" className={cx('icon')} />
|
||||
</Text>
|
||||
|
|
@ -167,11 +168,11 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
|
|||
<img src={qrCodeImage} className={cx('disconnect__qrCode')} />
|
||||
<DisconnectButton onClick={disconnectMobileApp} />
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
);
|
||||
} else if (QRCodeValue) {
|
||||
content = (
|
||||
<VerticalGroup spacing="lg">
|
||||
<Stack direction="column" gap={StackSize.lg}>
|
||||
<Text type="primary" strong>
|
||||
Sign in via QR Code
|
||||
</Text>
|
||||
|
|
@ -191,14 +192,14 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
|
|||
</a>
|
||||
</Text>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Mobile App Connection</h3>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<div className={cx('container')}>
|
||||
{QRCodeDataParsed && isMobile && (
|
||||
<Block shadowed bordered withBackground className={cx('container__box')}>
|
||||
|
|
@ -214,7 +215,7 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
|
|||
</div>
|
||||
{mobileAppIsCurrentlyConnected && isCurrentUser && !disconnectingMobileApp && (
|
||||
<div className={cx('notification-buttons')}>
|
||||
<HorizontalGroup spacing={'md'} justify={'flex-end'}>
|
||||
<Stack gap={StackSize.md} justifyContent={'flex-end'}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => onSendTestNotification()}
|
||||
|
|
@ -229,17 +230,17 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
|
|||
>
|
||||
Send Test Push Important
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
||||
function renderConnectToCloud() {
|
||||
return (
|
||||
<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>
|
||||
<WithPermissionControlDisplay
|
||||
userAction={UserActions.OtherSettingsWrite}
|
||||
|
|
@ -252,7 +253,7 @@ export const MobileAppConnection = observer(({ userPk }: Props) => {
|
|||
</Button>
|
||||
</PluginLink>
|
||||
</WithPermissionControlDisplay>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</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';
|
||||
|
||||
describe('DisconnectButton', () => {
|
||||
test('it renders properly', () => {
|
||||
const component = render(<DisconnectButton onClick={() => {}} />);
|
||||
expect(component.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('It calls the onClick handler when clicked', async () => {
|
||||
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 { VerticalGroup } from '@grafana/ui';
|
||||
import { Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import AppleLogoSVG from 'assets/img/apple-logo.svg';
|
||||
import PlayStoreLogoSVG from 'assets/img/play-store-logo.svg';
|
||||
import { Block } from 'components/GBlock/Block';
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
import styles from './DownloadIcons.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
export const DownloadIcons: FC = () => (
|
||||
<VerticalGroup spacing="lg">
|
||||
<Stack direction="column" gap={StackSize.lg}>
|
||||
<Text type="primary" strong>
|
||||
Download
|
||||
</Text>
|
||||
<Text type="primary">The Grafana OnCall app is available on both the App Store and Google Play Store.</Text>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<a
|
||||
style={{ width: '100%' }}
|
||||
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
|
||||
|
|
@ -45,6 +46,6 @@ export const DownloadIcons: FC = () => (
|
|||
</Text>
|
||||
</Block>
|
||||
</a>
|
||||
</VerticalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</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 { Button, VerticalGroup } from '@grafana/ui';
|
||||
import { Button, Stack } from '@grafana/ui';
|
||||
|
||||
import { Text } from 'components/Text/Text';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
type Props = {
|
||||
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}`;
|
||||
|
||||
return (
|
||||
<VerticalGroup spacing="lg">
|
||||
<Stack direction="column" gap={StackSize.lg}>
|
||||
<Text type="primary" strong>
|
||||
Sign in via deeplink
|
||||
</Text>
|
||||
|
|
@ -27,6 +28,6 @@ export const LinkLoginButton: FC<Props> = (props: Props) => {
|
|||
>
|
||||
Connect Mobile App
|
||||
</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 {
|
||||
Button,
|
||||
ConfirmModal,
|
||||
ConfirmModalProps,
|
||||
Drawer,
|
||||
HorizontalGroup,
|
||||
Input,
|
||||
Tab,
|
||||
TabsBar,
|
||||
VerticalGroup,
|
||||
} from '@grafana/ui';
|
||||
import { Button, ConfirmModal, ConfirmModalProps, Drawer, Input, Tab, TabsBar, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import { FormProvider, useForm, useFormContext } from 'react-hook-form';
|
||||
|
|
@ -212,7 +202,7 @@ const Presets = (props: PresetsProps) => {
|
|||
return (
|
||||
<Drawer scrollableContent title="New Outgoing Webhook" onClose={onHide} closeOnMaskClick={false} width="640px">
|
||||
<div className={cx('content')}>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Text type="secondary">
|
||||
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
|
||||
|
|
@ -231,7 +221,7 @@ const Presets = (props: PresetsProps) => {
|
|||
)}
|
||||
|
||||
<WebhookPresetBlocks presets={presets} onBlockClick={onSelect} />
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
|
|
@ -265,7 +255,7 @@ const NewWebhook = (props: NewWebhookProps) => {
|
|||
onTemplateEditClick={onTemplateEditClick}
|
||||
/>
|
||||
<div className={cx('buttons')}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Stack justifyContent="flex-end">
|
||||
{action === WebhookFormActionType.NEW ? (
|
||||
<Button variant="secondary" onClick={onBack}>
|
||||
Back
|
||||
|
|
@ -280,7 +270,7 @@ const NewWebhook = (props: NewWebhookProps) => {
|
|||
Create
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -400,7 +390,7 @@ const WebhookTabsContent: React.FC<WebhookTabsProps> = observer(
|
|||
onTemplateEditClick={onTemplateEditClick}
|
||||
/>
|
||||
<div className={cx('buttons')}>
|
||||
<HorizontalGroup justify={'flex-end'}>
|
||||
<Stack justifyContent={'flex-end'}>
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
@ -428,7 +418,7 @@ const WebhookTabsContent: React.FC<WebhookTabsProps> = observer(
|
|||
{action === WebhookFormActionType.NEW ? 'Create' : 'Update'}
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ export const OutgoingWebhookFormFields: React.FC<OutgoingWebhookFormFieldsProps>
|
|||
placeholder="Choose (Optional)"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
dataTestId="team-selector"
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
|
@ -128,6 +129,7 @@ export const OutgoingWebhookFormFields: React.FC<OutgoingWebhookFormFieldsProps>
|
|||
error={errors.trigger_type?.message}
|
||||
>
|
||||
<Select
|
||||
data-testid="triggerType-selector"
|
||||
placeholder="Choose (Required)"
|
||||
value={field.value}
|
||||
menuShouldPortal
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { EmptySearchResult, HorizontalGroup, VerticalGroup } from '@grafana/ui';
|
||||
import { EmptySearchResult, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -11,6 +11,7 @@ import { Text } from 'components/Text/Text';
|
|||
import { getWebhookPresetIcons } from 'containers/OutgoingWebhookForm/WebhookPresetIcons.config';
|
||||
import { OutgoingWebhookPreset } from 'models/outgoing_webhook/outgoing_webhook.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { StackSize } from 'utils/consts';
|
||||
|
||||
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')}>
|
||||
<div className={cx('card-bg')}>{logo}</div>
|
||||
<div className={cx('title')}>
|
||||
<VerticalGroup spacing="xs">
|
||||
<HorizontalGroup>
|
||||
<Stack direction="column" gap={StackSize.xs}>
|
||||
<Stack>
|
||||
<Text strong data-testid="webhook-preset-display-name">
|
||||
{preset.name}
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
<Text type="secondary" size="small">
|
||||
{preset.description}
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</Block>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { HorizontalGroup, Button } from '@grafana/ui';
|
||||
import { Button, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -30,11 +30,11 @@ export const OutgoingWebhookStatus = observer(({ id, closeDrawer }: OutgoingWebh
|
|||
<div className={cx('content')}>
|
||||
<WebhookLastEventDetails webhook={webhook} sourceCodeRootClassName={cx('sourceCodeRoot')} />
|
||||
<div className={commonStyles.bottomDrawerButtons}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Stack justifyContent="flex-end">
|
||||
<Button variant="secondary" onClick={closeDrawer}>
|
||||
Close
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { get } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -65,7 +65,7 @@ export const PersonalNotificationSettings = observer((props: PersonalNotificatio
|
|||
const allNotificationPolicies = userStore.notificationPolicies[userPk];
|
||||
const title = (
|
||||
<Text.Title level={5}>
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
{isImportant ? 'Important Notifications' : 'Default Notifications'}
|
||||
<Tooltip
|
||||
placement="top"
|
||||
|
|
@ -77,7 +77,7 @@ export const PersonalNotificationSettings = observer((props: PersonalNotificatio
|
|||
>
|
||||
<Icon name="info-circle" size="md"></Icon>
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Text.Title>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||
|
||||
import { css } from '@emotion/css';
|
||||
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 { Controller, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||
|
|
@ -41,12 +41,12 @@ export const PluginConfigPage = observer((props: PluginConfigPageProps<PluginMet
|
|||
});
|
||||
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Text.Title level={3} className="u-margin-bottom-md">
|
||||
Configure Grafana OnCall
|
||||
</Text.Title>
|
||||
{getIsRunningOpenSourceVersion() ? <OSSPluginConfigPage {...props} /> : <CloudPluginConfigPage {...props} />}
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ const CloudPluginConfigPage = observer(
|
|||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Text type="secondary" className={styles.secondaryTitle}>
|
||||
This is a cloud-managed configuration.
|
||||
</Text>
|
||||
|
|
@ -67,7 +67,7 @@ const CloudPluginConfigPage = observer(
|
|||
shouldRender={!isPluginConnected}
|
||||
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>
|
||||
</a>
|
||||
</Text>
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={recreateServiceAccountAndRecheckPluginStatus}
|
||||
|
|
@ -147,7 +147,7 @@ const OSSPluginConfigPage = observer(
|
|||
shouldRender={isRecreatingServiceAccount}
|
||||
render={() => <LoadingPlaceholder text="" className={styles.spinner} />}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ const OSSPluginConfigPage = observer(
|
|||
</Field>
|
||||
)}
|
||||
/>
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
{isPluginConnected && (
|
||||
<Button onClick={() => navigate(`${PLUGIN_ROOT}/${DEFAULT_PAGE}`)}>Open Grafana OnCall</Button>
|
||||
)}
|
||||
|
|
@ -194,7 +194,7 @@ const OSSPluginConfigPage = observer(
|
|||
shouldRender={isReinitializating}
|
||||
render={() => <LoadingPlaceholder text="" className={styles.spinner} />}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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 { useHistory } from 'react-router-dom';
|
||||
|
||||
|
|
@ -19,9 +19,9 @@ export const PluginInitializer: FC<PluginInitializerProps> = observer(({ childre
|
|||
|
||||
if (isCheckingConnectionStatus) {
|
||||
return (
|
||||
<VerticalGroup justify="center" height="100%" align="center">
|
||||
<Stack direction="column" justifyContent="center" height="100%" alignItems="center">
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
|
@ -57,13 +57,13 @@ const PluginNotConnectedFullPageError = observer(() => {
|
|||
</>
|
||||
}
|
||||
>
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<Button variant="secondary" onClick={() => window.location.reload()}>
|
||||
Retry
|
||||
</Button>
|
||||
{!isOpenSource && <Button onClick={() => window.open(REQUEST_HELP_URL, '_blank')}>Request help</Button>}
|
||||
{isOpenSource && isCurrentUserAdmin && <Button onClick={() => push(PLUGIN_CONFIG)}>Open configuration</Button>}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</FullPageError>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
Tooltip,
|
||||
Button,
|
||||
withTheme2,
|
||||
Themeable2,
|
||||
} from '@grafana/ui';
|
||||
import { capitalCase } from 'change-case';
|
||||
import { debounce, isUndefined, omitBy, pickBy } from 'lodash-es';
|
||||
|
|
@ -38,12 +37,13 @@ import { parseFilters } from './RemoteFilters.helpers';
|
|||
import { FilterOption } from './RemoteFilters.types';
|
||||
import { TimeRangePickerWrapper } from './TimeRangePickerWrapper';
|
||||
|
||||
interface RemoteFiltersProps extends WithStoreProps, Themeable2 {
|
||||
interface RemoteFiltersProps extends WithStoreProps {
|
||||
onChange: (filters: Record<string, any>, isOnMount: boolean, invalidateFn: () => boolean) => void;
|
||||
query: KeyValue;
|
||||
page: PAGE;
|
||||
grafanaTeamStore: GrafanaTeamStore;
|
||||
extraInformation?: FilterExtraInformation;
|
||||
theme: GrafanaTheme2;
|
||||
extraFilters?: (state, setState, onFiltersValueChange) => React.ReactNode;
|
||||
skipFilterOptionFn?: (filterOption: FilterOption) => boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { FC, useMemo } from 'react';
|
||||
|
||||
import { HorizontalGroup, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { LoadingPlaceholder, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -177,9 +177,9 @@ export const Rotation: FC<RotationProps> = observer((props) => {
|
|||
<Empty text={emptyText} />
|
||||
)
|
||||
) : (
|
||||
<HorizontalGroup align="center" justify="center">
|
||||
<Stack alignItems="center" justifyContent="center">
|
||||
<LoadingPlaceholder text="Loading shifts..." />
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,6 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Field,
|
||||
HorizontalGroup,
|
||||
Icon,
|
||||
IconButton,
|
||||
InlineSwitch,
|
||||
Select,
|
||||
Switch,
|
||||
Tooltip,
|
||||
VerticalGroup,
|
||||
} from '@grafana/ui';
|
||||
import { Alert, Button, Field, Icon, IconButton, InlineSwitch, Select, Switch, Tooltip, Stack } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
import { observer } from 'mobx-react';
|
||||
|
|
@ -63,7 +51,7 @@ import {
|
|||
toDateWithTimezoneOffsetAtMidnight,
|
||||
} from 'pages/schedule/Schedule.helpers';
|
||||
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 styles from './RotationForm.module.css';
|
||||
|
|
@ -552,14 +540,14 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
>
|
||||
<div className={cx('root')} data-testid="rotation-form">
|
||||
<div>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="sm">
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack gap={StackSize.sm}>
|
||||
{shiftId === 'new' && <Tag color={shiftColor}>New</Tag>}
|
||||
<Text.Title editModalTitle="Rotation name" onTextChange={handleRotationNameChange} level={5} editable>
|
||||
{rotationName}
|
||||
</Text.Title>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
</Stack>
|
||||
<Stack>
|
||||
{shiftId !== 'new' && (
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
|
|
@ -575,16 +563,16 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
tooltip={shiftId === 'new' ? 'Cancel' : 'Close'}
|
||||
onClick={onHide}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
<div className={cx('container')}>
|
||||
<div className={cx('content')}>
|
||||
<VerticalGroup spacing="none">
|
||||
<Stack direction="column" gap={StackSize.none}>
|
||||
{hasUpdatedShift && (
|
||||
<Block bordered className={cx('updated-shift-info')}>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<Stack direction="column">
|
||||
<Stack alignItems="flex-start">
|
||||
<Icon name="info-circle" size="md"></Icon>
|
||||
<Text>
|
||||
This rotation is read-only because it has newer version.{' '}
|
||||
|
|
@ -593,15 +581,15 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
</Text>{' '}
|
||||
instead
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Block>
|
||||
)}
|
||||
{!hasUpdatedShift && ended && (
|
||||
<div className={cx('updated-shift-info')}>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Alert severity="info" title={(<Text>This rotation is over</Text>) as unknown as string} />
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
<div className={cx('two-fields')}>
|
||||
|
|
@ -623,7 +611,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
</Field>
|
||||
<Field
|
||||
label={
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Stack gap={StackSize.xs}>
|
||||
<Text type="primary" size="small">
|
||||
Ends
|
||||
</Text>
|
||||
|
|
@ -634,7 +622,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
onChange={handleChangeEndless}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
}
|
||||
data-testid="rotation-end"
|
||||
>
|
||||
|
|
@ -658,14 +646,14 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
invalid={Boolean(errors.interval)}
|
||||
error={'Invalid recurrence period'}
|
||||
label={
|
||||
<HorizontalGroup spacing="sm">
|
||||
<Stack gap={StackSize.sm}>
|
||||
<Text type="primary" size="small">
|
||||
Recurrence period
|
||||
</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.">
|
||||
<Icon name="info-circle" size="md"></Icon>
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
|
|
@ -687,11 +675,11 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<VerticalGroup spacing="md">
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<Stack direction="column" gap={StackSize.md}>
|
||||
<Stack direction="column">
|
||||
<Stack alignItems="flex-start">
|
||||
<Switch disabled={disabled} value={isMaskedByWeekdays} onChange={onMaskedByWeekdaysSwitch} />
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Text type="secondary">Mask by weekdays</Text>
|
||||
{isMaskedByWeekdays && (
|
||||
<DaysSelector
|
||||
|
|
@ -702,16 +690,16 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<HorizontalGroup align="flex-start">
|
||||
<Stack alignItems="flex-start">
|
||||
<Switch
|
||||
disabled={isSelectedPartOfDayDisabled()}
|
||||
value={isLimitShiftEnabled}
|
||||
onChange={onLimitShiftSwitch}
|
||||
/>
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
<Text type="secondary">Limit each shift length</Text>
|
||||
{isLimitShiftEnabled && (
|
||||
<ShiftPeriod
|
||||
|
|
@ -736,17 +724,17 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
will repeat every day
|
||||
</Text>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<Text size="small">Users</Text>
|
||||
<Tooltip content="By default each new user creates new rotation group. You can customise groups by dragging.">
|
||||
<Icon name="info-circle" size="md" />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
<UserGroups
|
||||
disabled={disabled}
|
||||
|
|
@ -763,15 +751,15 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
)}
|
||||
showError={Boolean(errors.rolling_users)}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Stack justifyContent="space-between">
|
||||
<Text type="secondary">
|
||||
Current timezone: <Text type="primary">{store.timezoneStore.selectedTimezoneLabel}</Text>
|
||||
</Text>
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
{shiftId !== 'new' && (
|
||||
<Tooltip content="Stop the current rotation and start a new one">
|
||||
<Button disabled={disabled} variant="secondary" onClick={updateAsNew}>
|
||||
|
|
@ -790,8 +778,8 @@ export const RotationForm = observer((props: RotationFormProps) => {
|
|||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
@ -929,9 +917,9 @@ const ShiftPeriod = ({
|
|||
}, [unitToCreate]);
|
||||
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<Stack direction="column">
|
||||
{timeUnits.map((unit, index: number, arr) => (
|
||||
<HorizontalGroup key={unit.unit}>
|
||||
<Stack key={unit.unit}>
|
||||
<TimeUnitSelector
|
||||
disabled={disabled}
|
||||
unit={unit.unit}
|
||||
|
|
@ -960,7 +948,7 @@ const ShiftPeriod = ({
|
|||
onClick={handleTimeUnitAdd}
|
||||
/>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
))}
|
||||
{timeUnits.length === 0 && unitToCreate !== undefined && (
|
||||
<Button disabled={disabled} variant="secondary" icon="plus" size="sm" onClick={handleTimeUnitAdd}>
|
||||
|
|
@ -969,6 +957,6 @@ const ShiftPeriod = ({
|
|||
)}
|
||||
<Text type="secondary">({duration || '0m'})</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 { IconButton, VerticalGroup, HorizontalGroup, Field, Button, useTheme2 } from '@grafana/ui';
|
||||
import { IconButton, Stack, Field, Button, useTheme2 } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import dayjs from 'dayjs';
|
||||
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 { getDateTime, getUTCString, toDateWithTimezoneOffset } from 'pages/schedule/Schedule.helpers';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { StackSize } from 'utils/consts';
|
||||
import { useDebouncedCallback, useResize } from 'utils/hooks';
|
||||
|
||||
import { getDraggableModalCoordinatesOnInit } from './RotationForm.helpers';
|
||||
|
|
@ -219,15 +220,15 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
</Draggable>
|
||||
)}
|
||||
>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup spacing="sm">
|
||||
<Stack direction="column">
|
||||
<Stack justifyContent="space-between">
|
||||
<Stack gap={StackSize.sm}>
|
||||
{shiftId === 'new' && <Tag color={shiftColor}>New</Tag>}
|
||||
<Text.Title onTextChange={handleRotationNameChange} level={5} editable>
|
||||
{rotationName}
|
||||
</Text.Title>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
</Stack>
|
||||
<Stack>
|
||||
{shiftId !== 'new' && (
|
||||
<WithConfirm title="Are you sure you want to delete override?">
|
||||
<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'}
|
||||
onClick={onHide}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<div className={cx('container')}>
|
||||
<div className={cx('override-form-content')} data-testid="override-inputs">
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup align="flex-start">
|
||||
<Stack direction="column">
|
||||
<Stack alignItems="flex-start">
|
||||
<Field
|
||||
className={cx('date-time-picker')}
|
||||
data-testid="override-start"
|
||||
|
|
@ -282,7 +283,7 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
error={errors.shift_end}
|
||||
/>
|
||||
</Field>
|
||||
</HorizontalGroup>
|
||||
</Stack>
|
||||
|
||||
<UserGroups
|
||||
disabled={disabled}
|
||||
|
|
@ -299,20 +300,20 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
|
|||
)}
|
||||
showError={Boolean(errors.rolling_users)}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Stack justifyContent="space-between">
|
||||
<Text type="secondary">
|
||||
Current timezone: <Text type="primary">{store.timezoneStore.selectedTimezoneLabel}</Text>
|
||||
</Text>
|
||||
<HorizontalGroup>
|
||||
<Stack>
|
||||
<Button variant="primary" onClick={handleCreate} disabled={disabled || !isFormValid}>
|
||||
{shiftId === 'new' ? 'Create' : 'Update'}
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</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