Configure knip and remove dead code (#3999)
# What this PR does - provide a way to detect dead code and remove it ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --------- Co-authored-by: Joey Orlando <joseph.t.orlando@gmail.com>
This commit is contained in:
parent
5326d945e0
commit
3eaeabdddf
20 changed files with 1212 additions and 977 deletions
4
grafana-plugin/knip.json
Normal file
4
grafana-plugin/knip.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"entry": ["src/PluginPage.tsx", "src/module.ts"],
|
||||||
|
"project": ["**/*.{js,ts,jsx,tsx}"]
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,8 @@
|
||||||
"plop": "plop",
|
"plop": "plop",
|
||||||
"setversion": "setversion",
|
"setversion": "setversion",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"typecheck:watch": "yarn typecheck --watch --preserveWatchOutput false"
|
"typecheck:watch": "yarn typecheck --watch --preserveWatchOutput false",
|
||||||
|
"find-dead-code": "knip"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -89,6 +90,7 @@
|
||||||
"identity-obj-proxy": "3.0.0",
|
"identity-obj-proxy": "3.0.0",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"jest-environment-jsdom": "^29.5.0",
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
|
"knip": "^5.0.3",
|
||||||
"lint-staged": "^10.2.11",
|
"lint-staged": "^10.2.11",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mailslurp-client": "^15.14.1",
|
"mailslurp-client": "^15.14.1",
|
||||||
|
|
@ -141,7 +143,7 @@
|
||||||
"change-case": "^4.1.1",
|
"change-case": "^4.1.1",
|
||||||
"circular-dependency-plugin": "^5.2.2",
|
"circular-dependency-plugin": "^5.2.2",
|
||||||
"dayjs": "^1.11.5",
|
"dayjs": "^1.11.5",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"mobx": "6.12.0",
|
"mobx": "6.12.0",
|
||||||
"mobx-react": "9.1.0",
|
"mobx-react": "9.1.0",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export const getBackendSrv = () => ({
|
|
||||||
get: jest.fn(),
|
|
||||||
post: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { capitalCase } from 'change-case';
|
|
||||||
|
|
||||||
export function getLabelFromTemplateName(templateName: string, group: any) {
|
|
||||||
let arrayFromName = capitalCase(templateName).split(' ', 4);
|
|
||||||
let arrayWithNeededValues;
|
|
||||||
if (group === 'alert behaviour') {
|
|
||||||
arrayWithNeededValues = arrayFromName.slice(0, arrayFromName.lastIndexOf('Template'));
|
|
||||||
} else {
|
|
||||||
arrayWithNeededValues = arrayFromName.slice(1, arrayFromName.lastIndexOf('Template'));
|
|
||||||
}
|
|
||||||
return arrayWithNeededValues.join(' ');
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
|
||||||
import { HorizontalGroup, getTagColorsFromName, useStyles2 } from '@grafana/ui';
|
|
||||||
import tinycolor2 from 'tinycolor2';
|
|
||||||
|
|
||||||
export interface LabelTagProps {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
size?: LabelTagSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LabelTagSize = 'md' | 'sm';
|
|
||||||
|
|
||||||
export const LabelTag: React.FC<LabelTagProps> = (props: LabelTagProps) => {
|
|
||||||
const { label, value, size = 'sm' } = props;
|
|
||||||
|
|
||||||
const color = getLabelColor(label);
|
|
||||||
|
|
||||||
const styles = useStyles2((theme) => getStyles(theme, color, size));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.wrapper} role="listitem">
|
|
||||||
<HorizontalGroup spacing="none">
|
|
||||||
<div className={styles.label}>{label ?? ''}</div>
|
|
||||||
<div className={styles.value}>{value}</div>
|
|
||||||
</HorizontalGroup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function getLabelColor(input: string): string {
|
|
||||||
return getTagColorsFromName(input).color;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2, color?: string, size?: string) => {
|
|
||||||
const backgroundColor = color ?? theme.colors.secondary.main;
|
|
||||||
|
|
||||||
const borderColor = theme.isDark
|
|
||||||
? tinycolor2(backgroundColor).lighten(5).toString()
|
|
||||||
: tinycolor2(backgroundColor).darken(5).toString();
|
|
||||||
|
|
||||||
const valueBackgroundColor = theme.isDark
|
|
||||||
? tinycolor2(backgroundColor).darken(5).toString()
|
|
||||||
: tinycolor2(backgroundColor).lighten(5).toString();
|
|
||||||
|
|
||||||
const fontColor = color
|
|
||||||
? tinycolor2.mostReadable(backgroundColor, ['#000', '#fff']).toString()
|
|
||||||
: theme.colors.text.primary;
|
|
||||||
|
|
||||||
const padding =
|
|
||||||
size === 'md' ? `${theme.spacing(0.33)} ${theme.spacing(1)}` : `${theme.spacing(0.2)} ${theme.spacing(0.6)}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
wrapper: css`
|
|
||||||
color: ${fontColor};
|
|
||||||
font-size: ${theme.typography.bodySmall.fontSize};
|
|
||||||
|
|
||||||
border-radius: ${theme.shape.radius.default};
|
|
||||||
`,
|
|
||||||
label: css`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: inherit;
|
|
||||||
|
|
||||||
padding: ${padding};
|
|
||||||
background: ${backgroundColor};
|
|
||||||
|
|
||||||
border: solid 1px ${borderColor};
|
|
||||||
border-top-left-radius: ${theme.shape.radius.default};
|
|
||||||
border-bottom-left-radius: ${theme.shape.radius.default};
|
|
||||||
`,
|
|
||||||
value: css`
|
|
||||||
color: inherit;
|
|
||||||
padding: ${padding};
|
|
||||||
background: ${valueBackgroundColor};
|
|
||||||
|
|
||||||
border: solid 1px ${borderColor};
|
|
||||||
border-left: none;
|
|
||||||
border-top-right-radius: ${theme.shape.radius.default};
|
|
||||||
border-bottom-right-radius: ${theme.shape.radius.default};
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { PENDING_COLOR, Tooltip, Icon } from '@grafana/ui';
|
|
||||||
|
|
||||||
import { Schedule } from 'models/schedule/schedule.types';
|
|
||||||
|
|
||||||
interface ScheduleWarningProps {
|
|
||||||
item: Schedule;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ScheduleWarning = (props: ScheduleWarningProps) => {
|
|
||||||
const { item } = props;
|
|
||||||
if (item.warnings.length > 0) {
|
|
||||||
const tooltipContent = (
|
|
||||||
<div>
|
|
||||||
{item.warnings.map((warning: string, key: number) => (
|
|
||||||
<p key={key}>{warning}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<Tooltip placement="top" content={tooltipContent}>
|
|
||||||
<Icon style={{ color: PENDING_COLOR }} name="exclamation-triangle" />
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
import React, { ChangeEvent, useCallback } from 'react';
|
|
||||||
|
|
||||||
import { Field, Icon, Input, RadioButtonGroup } from '@grafana/ui';
|
|
||||||
import cn from 'classnames/bind';
|
|
||||||
|
|
||||||
import { ScheduleType } from 'models/schedule/schedule.types';
|
|
||||||
|
|
||||||
import styles from './SchedulesFilters.module.scss';
|
|
||||||
import { SchedulesFiltersType } from './SchedulesFilters.types';
|
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
|
||||||
|
|
||||||
interface SchedulesFiltersProps {
|
|
||||||
value: SchedulesFiltersType;
|
|
||||||
onChange: (filters: SchedulesFiltersType) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SchedulesFilters = (props: SchedulesFiltersProps) => {
|
|
||||||
const { value, onChange } = props;
|
|
||||||
|
|
||||||
const onSearchTermChangeCallback = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onChange({ ...value, searchTerm: e.currentTarget.value });
|
|
||||||
},
|
|
||||||
[value]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMineChange = useCallback(
|
|
||||||
(mine) => {
|
|
||||||
onChange({ ...value, mine });
|
|
||||||
},
|
|
||||||
[value]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleStatusChange = useCallback(
|
|
||||||
(used) => {
|
|
||||||
onChange({ ...value, used });
|
|
||||||
},
|
|
||||||
[value]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTypeChange = useCallback(
|
|
||||||
(type) => {
|
|
||||||
onChange({ ...value, type });
|
|
||||||
},
|
|
||||||
[value]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={cx('left')}>
|
|
||||||
<Field label="Search by name">
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
className={cx('search')}
|
|
||||||
prefix={<Icon name="search" />}
|
|
||||||
placeholder="Search..."
|
|
||||||
value={value.searchTerm}
|
|
||||||
onChange={onSearchTermChangeCallback}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
<div className={cx('right')}>
|
|
||||||
<Field label="Mine">
|
|
||||||
<RadioButtonGroup
|
|
||||||
options={[
|
|
||||||
{ label: 'All', value: undefined },
|
|
||||||
{
|
|
||||||
label: 'Mine',
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Not mine',
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={value.mine}
|
|
||||||
onChange={handleMineChange}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Status">
|
|
||||||
<RadioButtonGroup
|
|
||||||
options={[
|
|
||||||
{ label: 'All', value: undefined },
|
|
||||||
{
|
|
||||||
label: 'Used in escalations',
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
{ label: 'Unused', value: false },
|
|
||||||
]}
|
|
||||||
value={value.used}
|
|
||||||
onChange={handleStatusChange}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Type">
|
|
||||||
<RadioButtonGroup
|
|
||||||
options={[
|
|
||||||
{ label: 'All', value: undefined },
|
|
||||||
{
|
|
||||||
label: 'Web',
|
|
||||||
value: ScheduleType.API,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'ICal',
|
|
||||||
value: ScheduleType.Ical,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'API',
|
|
||||||
value: ScheduleType.Calendar,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={value?.type}
|
|
||||||
onChange={handleTypeChange}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { ScheduleType } from 'models/schedule/schedule.types';
|
|
||||||
|
|
||||||
export interface SchedulesFiltersType {
|
|
||||||
searchTerm: string;
|
|
||||||
type: ScheduleType;
|
|
||||||
used: boolean | undefined;
|
|
||||||
mine: boolean | undefined;
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
.root {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
& .search {
|
|
||||||
width: 320px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import React, { ChangeEvent, useCallback } from 'react';
|
|
||||||
|
|
||||||
import { Icon, Input } from '@grafana/ui';
|
|
||||||
import cn from 'classnames/bind';
|
|
||||||
|
|
||||||
import styles from './SearchInput.module.scss';
|
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
|
||||||
|
|
||||||
interface SearchInputProps {
|
|
||||||
value: any;
|
|
||||||
onChange: (filters: any) => void;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchInput = (props: SearchInputProps) => {
|
|
||||||
const { value = { searchTerm: '' }, onChange, className } = props;
|
|
||||||
|
|
||||||
const onSearchTermChangeCallback = useCallback(
|
|
||||||
(e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const filters = {
|
|
||||||
...value,
|
|
||||||
searchTerm: e.currentTarget.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange(filters);
|
|
||||||
},
|
|
||||||
[onChange, value]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cx('root', className)}>
|
|
||||||
<Input
|
|
||||||
className={cx('search', 'control')}
|
|
||||||
placeholder="Search"
|
|
||||||
value={value.searchTerm}
|
|
||||||
onChange={onSearchTermChangeCallback}
|
|
||||||
suffix={<Icon name="search" />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
// TODO: Refactor to reuse these tag styles across multiple pages
|
|
||||||
$score-primary: rgba(27, 133, 94, 0.15);
|
|
||||||
$score-warning: rgba(245, 183, 61, 0.18);
|
|
||||||
$score-danger: rgba(209, 14, 92, 0.15);
|
|
||||||
|
|
||||||
.root {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heartbeat {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heartbeat-icon {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alertsInfoText {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 4px 10px 3px 10px;
|
|
||||||
|
|
||||||
&--danger {
|
|
||||||
background-color: $score-danger;
|
|
||||||
color: var(--tag-text-danger);
|
|
||||||
border: 1px solid var(--tag-border-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--warning {
|
|
||||||
background-color: $score-warning;
|
|
||||||
color: var(--tag-text-warning);
|
|
||||||
border: 1px solid var(--tag-border-warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag__icon {
|
|
||||||
&--danger {
|
|
||||||
color: var(--error-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--warning {
|
|
||||||
color: var(--warning-text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Badge, HorizontalGroup, IconButton, Tooltip, VerticalGroup } from '@grafana/ui';
|
|
||||||
import cn from 'classnames/bind';
|
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
|
||||||
import Emoji from 'react-emoji-render';
|
|
||||||
|
|
||||||
import { IntegrationLogo } from 'components/IntegrationLogo/IntegrationLogo';
|
|
||||||
import { PluginLink } from 'components/PluginLink/PluginLink';
|
|
||||||
import { Text } from 'components/Text/Text';
|
|
||||||
import { TeamName } from 'containers/TeamName/TeamName';
|
|
||||||
import { HeartGreenIcon, HeartRedIcon } from 'icons/Icons';
|
|
||||||
import { AlertReceiveChannelHelper } from 'models/alert_receive_channel/alert_receive_channel.helpers';
|
|
||||||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
|
||||||
import { useStore } from 'state/useStore';
|
|
||||||
|
|
||||||
import styles from './AlertReceiveChannelCard.module.scss';
|
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
|
||||||
|
|
||||||
interface AlertReceiveChannelCardProps {
|
|
||||||
id: ApiSchemas['AlertReceiveChannel']['id'];
|
|
||||||
onShowHeartbeatModal: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AlertReceiveChannelCard = observer((props: AlertReceiveChannelCardProps) => {
|
|
||||||
const { id, onShowHeartbeatModal } = props;
|
|
||||||
|
|
||||||
const store = useStore();
|
|
||||||
|
|
||||||
const { alertReceiveChannelStore, heartbeatStore, grafanaTeamStore } = store;
|
|
||||||
|
|
||||||
const alertReceiveChannel = alertReceiveChannelStore.items[id];
|
|
||||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[id];
|
|
||||||
|
|
||||||
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
|
|
||||||
const heartbeat = heartbeatStore.items[heartbeatId];
|
|
||||||
|
|
||||||
const heartbeatStatus = Boolean(heartbeat?.status);
|
|
||||||
|
|
||||||
const integration = AlertReceiveChannelHelper.getIntegrationSelectOption(
|
|
||||||
alertReceiveChannelStore,
|
|
||||||
alertReceiveChannel
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cx('root')}>
|
|
||||||
<HorizontalGroup align="flex-start">
|
|
||||||
<div className={cx('heartbeat')}>
|
|
||||||
{alertReceiveChannel.is_available_for_integration_heartbeat && (
|
|
||||||
<Tooltip
|
|
||||||
placement="top"
|
|
||||||
content={
|
|
||||||
heartbeat
|
|
||||||
? `Last heartbeat: ${heartbeat.last_heartbeat_time_verbal || 'never'}`
|
|
||||||
: 'Click to setup heartbeat'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={cx('heartbeat-icon')} onClick={onShowHeartbeatModal}>
|
|
||||||
{heartbeatStatus ? <HeartGreenIcon /> : <HeartRedIcon />}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<VerticalGroup spacing="xs">
|
|
||||||
<HorizontalGroup>
|
|
||||||
<Text type="primary" size="medium">
|
|
||||||
<Emoji className={cx('title')} text={alertReceiveChannel.verbal_name} />
|
|
||||||
</Text>
|
|
||||||
<CopyToClipboard text={alertReceiveChannel.id}>
|
|
||||||
<IconButton
|
|
||||||
variant="primary"
|
|
||||||
tooltip={
|
|
||||||
<div>
|
|
||||||
ID {alertReceiveChannel.id}
|
|
||||||
<br />
|
|
||||||
(click to copy ID to clipboard)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
tooltipPlacement="top"
|
|
||||||
name="info-circle"
|
|
||||||
/>
|
|
||||||
</CopyToClipboard>
|
|
||||||
{alertReceiveChannelCounter && (
|
|
||||||
<PluginLink
|
|
||||||
query={{ page: 'alert-groups', integration: alertReceiveChannel.id }}
|
|
||||||
className={cx('alertsInfoText')}
|
|
||||||
>
|
|
||||||
<Badge
|
|
||||||
text={alertReceiveChannelCounter?.alerts_count + '/' + alertReceiveChannelCounter?.alert_groups_count}
|
|
||||||
color={'blue'}
|
|
||||||
tooltip={
|
|
||||||
alertReceiveChannelCounter?.alerts_count +
|
|
||||||
' alert' +
|
|
||||||
(alertReceiveChannelCounter?.alerts_count === 1 ? '' : 's') +
|
|
||||||
' in ' +
|
|
||||||
alertReceiveChannelCounter?.alert_groups_count +
|
|
||||||
' alert group' +
|
|
||||||
(alertReceiveChannelCounter?.alert_groups_count === 1 ? '' : 's')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PluginLink>
|
|
||||||
)}
|
|
||||||
</HorizontalGroup>
|
|
||||||
<HorizontalGroup spacing="xs">
|
|
||||||
<IntegrationLogo scale={0.08} integration={integration} />
|
|
||||||
<Text type="secondary" size="small">
|
|
||||||
{integration?.display_name}
|
|
||||||
</Text>
|
|
||||||
</HorizontalGroup>
|
|
||||||
<TeamName team={grafanaTeamStore.items[alertReceiveChannel.team]} size="small" />
|
|
||||||
</VerticalGroup>
|
|
||||||
</HorizontalGroup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Icon, Label, Tooltip } from '@grafana/ui';
|
|
||||||
|
|
||||||
import { FormItemType } from 'components/GForm/GForm.types';
|
|
||||||
import { GrafanaTeamStore } from 'models/grafana_team/grafana_team';
|
|
||||||
import { generateAssignToTeamInputDescription } from 'utils/consts';
|
|
||||||
|
|
||||||
export const getForm = (grafanaTeamStore: GrafanaTeamStore) => ({
|
|
||||||
name: 'Integration',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
label: 'Name',
|
|
||||||
name: 'verbal_name',
|
|
||||||
type: FormItemType.Input,
|
|
||||||
placeholder: 'Integration Name',
|
|
||||||
validation: { required: true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Description',
|
|
||||||
name: 'description_short',
|
|
||||||
type: FormItemType.TextArea,
|
|
||||||
placeholder: 'Integration Description',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'team',
|
|
||||||
label: (
|
|
||||||
<Label>
|
|
||||||
<span>Assign to team</span>
|
|
||||||
<Tooltip content={generateAssignToTeamInputDescription('Integrations')} placement="right">
|
|
||||||
<Icon name="info-circle" />
|
|
||||||
</Tooltip>
|
|
||||||
</Label>
|
|
||||||
),
|
|
||||||
type: FormItemType.GSelect,
|
|
||||||
extra: {
|
|
||||||
items: grafanaTeamStore.items,
|
|
||||||
fetchItemsFn: grafanaTeamStore.updateItems,
|
|
||||||
fetchItemFn: grafanaTeamStore.fetchItemById,
|
|
||||||
getSearchResult: grafanaTeamStore.getSearchResult,
|
|
||||||
displayField: 'name',
|
|
||||||
valueField: 'id',
|
|
||||||
showSearch: true,
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '', // this will skip field it in the submitted form
|
|
||||||
label: 'Bi-directional Integration',
|
|
||||||
type: FormItemType.PlainLabel,
|
|
||||||
isHidden: (data) => data.integration !== 'servicenow',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'servicenow_url',
|
|
||||||
label: 'Service Now URL',
|
|
||||||
type: FormItemType.Input,
|
|
||||||
isHidden: (data) => data.integration !== 'servicenow',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'authorization_header',
|
|
||||||
label: 'Authorization Header',
|
|
||||||
type: FormItemType.Input,
|
|
||||||
isHidden: (data) => data.integration !== 'servicenow',
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'alert_manager',
|
|
||||||
type: FormItemType.Other,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'contact_point',
|
|
||||||
type: FormItemType.Other,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'is_existing',
|
|
||||||
type: FormItemType.Other,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'alerting',
|
|
||||||
type: FormItemType.Other,
|
|
||||||
render: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
import React, { useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { DateTime, dateTime, SelectableValue } from '@grafana/data';
|
|
||||||
import { Select, TimeOfDayPicker, VerticalGroup } from '@grafana/ui';
|
|
||||||
import cn from 'classnames/bind';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
import { Text } from 'components/Text/Text';
|
|
||||||
import { toDate } from 'containers/RotationForm/RotationForm.helpers';
|
|
||||||
import { Timezone } from 'models/timezone/timezone.types';
|
|
||||||
import { useStore } from 'state/useStore';
|
|
||||||
|
|
||||||
import styles from 'containers/RotationForm/RotationForm.module.css';
|
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
|
||||||
|
|
||||||
interface WeekdayTimePickerProps {
|
|
||||||
value: dayjs.Dayjs;
|
|
||||||
timezone: Timezone;
|
|
||||||
onWeekDayChange: (value: number) => void;
|
|
||||||
onTimeChange: (hh: number, mm: number, ss: number) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
hideWeekday?: boolean;
|
|
||||||
weekStart: string;
|
|
||||||
error?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const weekdays = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
|
|
||||||
|
|
||||||
export const WeekdayTimePicker = (props: WeekdayTimePickerProps) => {
|
|
||||||
const { value: propValue, timezone, hideWeekday, disabled, weekStart, onWeekDayChange, onTimeChange, error } = props;
|
|
||||||
|
|
||||||
const { scheduleStore } = useStore();
|
|
||||||
|
|
||||||
const value = useMemo(() => toDate(propValue, timezone), [propValue, timezone]);
|
|
||||||
|
|
||||||
const options = useMemo(() => {
|
|
||||||
const index = scheduleStore.byDayOptions.findIndex(
|
|
||||||
({ display_name }) => display_name.toLowerCase() === weekStart.toLowerCase()
|
|
||||||
);
|
|
||||||
return [...scheduleStore.byDayOptions.slice(index), ...scheduleStore.byDayOptions.slice(0, index)].map(
|
|
||||||
({ display_name, value }) => ({
|
|
||||||
label: display_name.substring(0, 3),
|
|
||||||
value: weekdays.findIndex((val) => val === value),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [weekStart]);
|
|
||||||
|
|
||||||
const handleWeekDayChange = useCallback(
|
|
||||||
({ value: newValue }: SelectableValue) => {
|
|
||||||
const oldIndex = options.findIndex(({ value: optionValue }) => optionValue === value.getDay());
|
|
||||||
const newIndex = options.findIndex(({ value: optionValue }) => optionValue === newValue);
|
|
||||||
|
|
||||||
onWeekDayChange(newIndex - oldIndex);
|
|
||||||
},
|
|
||||||
[options, value]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTimeChange = useCallback(
|
|
||||||
(newMoment: DateTime) => {
|
|
||||||
// @ts-ignore actually new newMoment has second method
|
|
||||||
onTimeChange(newMoment.hour(), newMoment.minute(), newMoment.second());
|
|
||||||
},
|
|
||||||
[value]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VerticalGroup>
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'nowrap', gap: '8px' }}>
|
|
||||||
{!hideWeekday && (
|
|
||||||
<div style={{ width: '58%' }} className={cx({ 'control--error': Boolean(error) })}>
|
|
||||||
<Select options={options} onChange={handleWeekDayChange} value={value.getDay()} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div style={{ width: hideWeekday ? '100%' : '42%' }} className={cx({ 'control--error': Boolean(error) })}>
|
|
||||||
<TimeOfDayPicker disabled={disabled} value={dateTime(value)} onChange={handleTimeChange} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{error && <Text type="danger">{error}</Text>}
|
|
||||||
</VerticalGroup>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
.slack-infoblock {
|
|
||||||
width: 725px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slack-infoblock input {
|
|
||||||
color: var(--primary-text-link);
|
|
||||||
}
|
|
||||||
|
|
||||||
.slack-icon {
|
|
||||||
width: 60px;
|
|
||||||
}
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import React, { FC } from 'react';
|
|
||||||
|
|
||||||
import { Button, VerticalGroup, Icon, Field, Input } from '@grafana/ui';
|
|
||||||
import cn from 'classnames/bind';
|
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
|
|
||||||
import { Block } from 'components/GBlock/Block';
|
|
||||||
import { Text } from 'components/Text/Text';
|
|
||||||
import { SlackNewIcon } from 'icons/Icons';
|
|
||||||
import { DOCS_SLACK_SETUP } from 'utils/consts';
|
|
||||||
|
|
||||||
import styles from './SlackInstructions.module.css';
|
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
|
||||||
|
|
||||||
interface SlackInstructionsProps {}
|
|
||||||
/* This component will be used when we will work on moving ENV variables to chat-ops, but we need to do work on backend side first */
|
|
||||||
export const SlackInstructions: FC<SlackInstructionsProps> = observer(() => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<VerticalGroup spacing="lg">
|
|
||||||
<Text.Title level={2}>Connect Slack workspace</Text.Title>
|
|
||||||
|
|
||||||
<Block bordered withBackground className={cx('slack-infoblock')}>
|
|
||||||
<VerticalGroup align="center" spacing="lg">
|
|
||||||
<SlackNewIcon />
|
|
||||||
<Text>You can manage alert groups in your Slack workspace. </Text>
|
|
||||||
<Text>Before start you need to connect your Slack bot to Grafana OnCall.</Text>
|
|
||||||
<Text type="secondary">
|
|
||||||
For bot creating instructions and additional information please read{' '}
|
|
||||||
<a href={DOCS_SLACK_SETUP} target="_blank" rel="noreferrer">
|
|
||||||
<Text type="link">our documentation</Text>
|
|
||||||
</a>
|
|
||||||
</Text>{' '}
|
|
||||||
</VerticalGroup>
|
|
||||||
</Block>
|
|
||||||
<Text>Setup environment</Text>
|
|
||||||
<Text>
|
|
||||||
Create OnCall Slack bot using{' '}
|
|
||||||
<a href={DOCS_SLACK_SETUP} target="_blank" rel="noreferrer">
|
|
||||||
<Text type="link">our instructions</Text>
|
|
||||||
</a>{' '}
|
|
||||||
and fill out app credentials below.
|
|
||||||
</Text>
|
|
||||||
<div className={cx('slack-infoblock')}>
|
|
||||||
<Field label="App ID">
|
|
||||||
<Input id="appId" onChange={() => {}} defaultValue={'appId'} />
|
|
||||||
</Field>
|
|
||||||
<Field label="Client secret">
|
|
||||||
<Input id="clientsecret" onChange={() => {}} defaultValue={'clientsecret'} />
|
|
||||||
</Field>
|
|
||||||
<Field label="Signing secret">
|
|
||||||
<Input id="signingsecret" onChange={() => {}} defaultValue={'signingsecret'} />
|
|
||||||
</Field>
|
|
||||||
<Field label="Redirect host">
|
|
||||||
<Input id="host" onChange={() => {}} defaultValue={'https://'} />
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
<Block bordered withBackground className={cx('slack-infoblock')}>
|
|
||||||
<Text type="secondary">
|
|
||||||
<Icon name="info-circle" /> Your host to Slack must start with “https://” and be publicly available (meaning
|
|
||||||
that it can be reached by Slack servers). If your host is private or local, you can use redirecting services
|
|
||||||
like Ngrok.
|
|
||||||
</Text>
|
|
||||||
</Block>
|
|
||||||
<Button onClick={() => {}}>Save environment</Button>
|
|
||||||
</VerticalGroup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
export const WebhooksDefaultAlertGroup = {
|
|
||||||
pk: '0',
|
|
||||||
event: {
|
|
||||||
type: 'resolve',
|
|
||||||
time: '2023-04-19T21:59:21.714058+00:00',
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
id: 'UVMX6YI9VY9PV',
|
|
||||||
username: 'admin',
|
|
||||||
email: 'admin@localhost',
|
|
||||||
},
|
|
||||||
alert_group: {
|
|
||||||
id: 'I6HNZGUFG4K11',
|
|
||||||
integration_id: 'CZ7URAT4V3QF2',
|
|
||||||
route_id: 'RKHXJKVZYYVST',
|
|
||||||
alerts_count: 1,
|
|
||||||
state: 'resolved',
|
|
||||||
created_at: '2023-04-19T21:53:48.231148Z',
|
|
||||||
resolved_at: '2023-04-19T21:59:21.714058Z',
|
|
||||||
acknowledged_at: '2023-04-19T21:54:39.029347Z',
|
|
||||||
title: 'Incident',
|
|
||||||
permalinks: {
|
|
||||||
slack: null,
|
|
||||||
telegram: null,
|
|
||||||
web: 'https://**********.grafana.net/a/grafana-oncall-app/alert-groups/I6HNZGUFG4K11',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
alert_group_id: 'I6HNZGUFG4K11',
|
|
||||||
alert_payload: {
|
|
||||||
endsAt: '0001-01-01T00:00:00Z',
|
|
||||||
labels: {
|
|
||||||
region: 'eu-1',
|
|
||||||
alertname: 'TestAlert',
|
|
||||||
},
|
|
||||||
status: 'firing',
|
|
||||||
startsAt: '2018-12-25T15:47:47.377363608Z',
|
|
||||||
annotations: {
|
|
||||||
description: 'This alert was sent by user for the demonstration purposes',
|
|
||||||
},
|
|
||||||
generatorURL: '',
|
|
||||||
},
|
|
||||||
integration: {
|
|
||||||
id: 'CZ7URAT4V3QF2',
|
|
||||||
type: 'webhook',
|
|
||||||
name: 'Main Integration - Webhook',
|
|
||||||
team: 'Webhooks Demo',
|
|
||||||
},
|
|
||||||
notified_users: [],
|
|
||||||
users_to_be_notified: [],
|
|
||||||
responses: {
|
|
||||||
WHP936BM1GPVHQ: {
|
|
||||||
id: '7Qw7TbPmzppRnhLvK3AdkQ',
|
|
||||||
created_at: '15:53:50',
|
|
||||||
status: 'new',
|
|
||||||
content: {
|
|
||||||
message: 'Ticket created!',
|
|
||||||
region: 'eu',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { observer } from 'mobx-react';
|
|
||||||
|
|
||||||
import { getUserNotificationsSummary } from 'models/user/user.helpers';
|
|
||||||
import { User } from 'models/user/user.types';
|
|
||||||
import { useStore } from 'state/useStore';
|
|
||||||
|
|
||||||
interface UserSummaryProps {
|
|
||||||
id: User['pk'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserSummary = observer((props: UserSummaryProps) => {
|
|
||||||
const { id } = props;
|
|
||||||
|
|
||||||
const store = useStore();
|
|
||||||
|
|
||||||
const { userStore } = store;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!userStore.items[id]) {
|
|
||||||
userStore.loadUser(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = userStore.items[id];
|
|
||||||
|
|
||||||
return getUserNotificationsSummary(user);
|
|
||||||
});
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import { EscalationChainsPage } from 'pages/escalation-chains/EscalationChains';
|
|
||||||
import { IncidentPage } from 'pages/incident/Incident';
|
|
||||||
import { IncidentsPage } from 'pages/incidents/Incidents';
|
|
||||||
import { Insights } from 'pages/insights/Insights';
|
|
||||||
import { OutgoingWebhooks } from 'pages/outgoing_webhooks/OutgoingWebhooks';
|
|
||||||
import { SchedulePage } from 'pages/schedule/Schedule';
|
|
||||||
import { SchedulesPage } from 'pages/schedules/Schedules';
|
|
||||||
import { SettingsPage } from 'pages/settings/SettingsPage';
|
|
||||||
import { ChatOpsPage } from 'pages/settings/tabs/ChatOps/ChatOps';
|
|
||||||
import { CloudPage } from 'pages/settings/tabs/Cloud/CloudPage';
|
|
||||||
import LiveSettingsPage from 'pages/settings/tabs/LiveSettings/LiveSettingsPage';
|
|
||||||
import { UsersPage } from 'pages/users/Users';
|
|
||||||
|
|
||||||
import { IntegrationsPage } from './integrations/Integrations';
|
|
||||||
|
|
||||||
export interface NavRoute {
|
|
||||||
id: string;
|
|
||||||
component: (props?: any) => JSX.Element;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const routes: { [id: string]: NavRoute } = [
|
|
||||||
{
|
|
||||||
component: IncidentsPage,
|
|
||||||
id: 'incidents',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: IncidentPage,
|
|
||||||
id: 'incident',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: UsersPage,
|
|
||||||
id: 'users',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: IntegrationsPage,
|
|
||||||
id: 'integrations',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: EscalationChainsPage,
|
|
||||||
id: 'escalations',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: SchedulesPage,
|
|
||||||
id: 'schedules',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: SchedulePage,
|
|
||||||
id: 'schedule',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: ChatOpsPage,
|
|
||||||
id: 'chat-ops',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: OutgoingWebhooks,
|
|
||||||
id: 'outgoing_webhooks',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: SettingsPage,
|
|
||||||
id: 'settings',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: LiveSettingsPage,
|
|
||||||
id: 'live-settings',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: CloudPage,
|
|
||||||
id: 'cloud',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: Insights,
|
|
||||||
id: 'insights',
|
|
||||||
},
|
|
||||||
].reduce((prev, current) => {
|
|
||||||
prev[current.id] = {
|
|
||||||
id: current.id,
|
|
||||||
component: current.component,
|
|
||||||
};
|
|
||||||
|
|
||||||
return prev;
|
|
||||||
}, {});
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue