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:
Rares Mardare 2024-08-27 12:37:30 +03:00 committed by GitHub
parent 3269c9b3a7
commit 0965c6ab75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
170 changed files with 1220 additions and 4110 deletions

View file

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

View file

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

View file

@ -12,7 +12,7 @@ module.exports = {
{
files: ['src/**/*.{ts,tsx}'],
rules: {
'deprecation/deprecation': 'off',
'deprecation/deprecation': 'warn',
},
parserOptions: {
project: './tsconfig.json',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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