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",
|
||||
"setversion": "setversion",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"typecheck:watch": "yarn typecheck --watch --preserveWatchOutput false"
|
||||
"typecheck:watch": "yarn typecheck --watch --preserveWatchOutput false",
|
||||
"find-dead-code": "knip"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -89,6 +90,7 @@
|
|||
"identity-obj-proxy": "3.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"knip": "^5.0.3",
|
||||
"lint-staged": "^10.2.11",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mailslurp-client": "^15.14.1",
|
||||
|
|
@ -141,7 +143,7 @@
|
|||
"change-case": "^4.1.1",
|
||||
"circular-dependency-plugin": "^5.2.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"mobx": "6.12.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