[UI] fix eslint/prettier warnings (#678)

* UI spring cleaning

- fix ~570 outstanding eslint warnings
- make eslint force user to correct warnings
- remove .css files that are not referenced
- remove dummy.tsx as it is not consumed anywhere
- remove a few functions that were "dead code" (ie. not consumed anywhere)
- remove commented out blocks of code that had no explanatory comments surrounding them

* add prettier to pre-commit configuration

* change ignoreRestSiblings to true

we have a few spots in the codebase where we destructure
an object key and then use something like ...restProps
setting this to true allows that

* upgrade from eslint 7.21.0 to 8.25.0
- add @grafana/eslint-config to dev dependencies and pre-commit eslint deps
- add @grafana/eslint-config peer dependencies to package.json

* fix remaining outstanding prettier warnings

* enable noUnusedLocals and noUnusedParameters and fix errors related to this

* make pre-commit complain about eslint warnings

* import from moment-timezone instead of moment

* fix react/display-name eslint warning

* add eslint-plugin-react-hooks to dev deps

this is a peer dependency from @grafana/eslint-config

* turn off react/prop-types

* temporarily turn off react-hooks/exhaustive-deps

add note that it will be turned back on and fixed in next PR

* fix unused import errors after rebase to dev

* fix more new prettier errors

* turn react/no-unescaped-entities eslint rule off

* address PR comment about useReducer

* remove includeTemplateGroup from src/components/AlertTemplates/AlertTemplatesForm.helper.tsx

* update arg typing for refreshPageError

* update handleSyncException typing

* fix strict equality in containers/IntegrationSettings/parts/Autoresolve.tsx

* enhance typing in components/AlertTemplates/AlertTemplatesForm.tsx

* revert small change per Maxim's comment
This commit is contained in:
Joey Orlando 2022-10-24 14:27:03 +02:00 committed by GitHub
parent a60e14c3c6
commit 6e5cb4e8a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
182 changed files with 822 additions and 1468 deletions

View file

@ -23,16 +23,26 @@ repos:
- flake8-tidy-imports
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v7.21.0
rev: v8.25.0
hooks:
- id: eslint
entry: bash -c 'cd grafana-plugin && eslint --fix ${@/grafana-plugin\//}' --
entry: bash -c 'cd grafana-plugin && eslint --max-warnings=0 --fix ${@/grafana-plugin\//}' --
types: [file]
files: ^grafana-plugin/src/.*\.(js|jsx|ts|tsx)$
additional_dependencies:
- eslint@7.21.0
- eslint@^8.25.0
- eslint-plugin-import@^2.25.4
- eslint-plugin-rulesdir@^0.2.1
- "@grafana/eslint-config@^5.0.0"
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v2.7.1"
hooks:
- id: prettier
types_or: [css, javascript, jsx, ts, tsx, json]
files: ^grafana-plugin/src
additional_dependencies:
- prettier@^2.7.1
- repo: https://github.com/thibaudcolas/pre-commit-stylelint
rev: v13.13.1
@ -43,4 +53,6 @@ repos:
files: ^grafana-plugin/src/.*\.css$
additional_dependencies:
- stylelint@^13.13.1
- stylelint-prettier@^2.0.0
- stylelint-config-standard@^22.0.0
- stylelint-config-prettier@^9.0.3

View file

@ -9,18 +9,7 @@ module.exports = {
'^assets|^components|^containers|^declare|^icons|^img|^interceptors|^models|^network|^pages|^services|^state|^utils',
},
rules: {
'no-unused-vars': ['warn', { vars: 'all', args: 'after-used', ignoreRestSiblings: false }],
'react/prop-types': 'warn',
'react/display-name': 'warn',
'react/jsx-key': 'warn',
'react-hooks/exhaustive-deps': 'off',
'react/no-unescaped-entities': 'warn',
'react/jsx-no-target-blank': 'warn',
'react-hooks/exhaustive-deps': 'warn',
'no-restricted-imports': 'warn',
eqeqeq: 'warn',
'no-duplicate-imports': 'error',
'rulesdir/no-relative-import-paths': ['error', { allowSameFolder: true }],
'import/order': [
'error',
{
@ -47,5 +36,33 @@ module.exports = {
'newlines-between': 'always',
},
],
'no-unused-vars': [
'warn',
{
vars: 'all',
args: 'after-used',
argsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'no-duplicate-imports': 'error',
'no-restricted-imports': 'warn',
'react/display-name': 'warn',
/**
* It appears as though the react/prop-types rule has a bug in it
* when your props extend an interface
* https://github.com/jsx-eslint/eslint-plugin-react/issues/3325
*/
'react/prop-types': 'off',
'react/jsx-key': 'warn',
'react/jsx-no-target-blank': 'warn',
'react/no-unescaped-entities': 'off',
/**
* TODO: react-hooks/exhaustive-deps is temporarily disabled
* this will be turned back on, and the warnings fixed, in a forthcoming PR
*/
'react-hooks/exhaustive-deps': 'off',
'rulesdir/no-relative-import-paths': ['error', { allowSameFolder: true }],
},
};

View file

@ -1,12 +1,14 @@
{
"extends": "stylelint-config-standard",
"rules": {
"block-no-empty": [true,{ "severity": "warning"}],
"extends": ["stylelint-config-standard", "stylelint-prettier/recommended"],
"plugins": ["stylelint-prettier"],
"rules": {
"block-no-empty": [true, { "severity": "warning" }],
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global"]
}
]
],
"prettier/prettier": true
}
}

View file

@ -3,8 +3,8 @@
"version": "1.0.0",
"description": "Grafana OnCall Plugin",
"scripts": {
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./src",
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --quiet ./src",
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 ./src",
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 --quiet ./src",
"stylelint": "stylelint ./src/**/*.css",
"stylelint:fix": "stylelint --fix ./src/**/*.css",
"build": "grafana-toolkit plugin:build",
@ -52,6 +52,7 @@
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@grafana/data": "^9.1.1",
"@grafana/eslint-config": "^5.0.0",
"@grafana/runtime": "^9.1.1",
"@grafana/toolkit": "^9.1.1",
"@grafana/ui": "^9.1.1",
@ -67,8 +68,13 @@
"@types/react-router-dom": "^5.3.3",
"@types/react-test-renderer": "^17.0.2",
"@types/throttle-debounce": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"copy-webpack-plugin": "^11.0.0",
"dompurify": "^2.3.12",
"eslint": "^8.25.0",
"eslint-plugin-jsdoc": "^39.3.14",
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-rulesdir": "^0.2.1",
"jest": "^27.5.1",
"jest-environment-jsdom": "^27.5.1",
@ -78,8 +84,11 @@
"plop": "^2.7.4",
"postcss-loader": "^7.0.1",
"react-test-renderer": "^17.0.2",
"stylelint-config-prettier": "^9.0.3",
"stylelint-prettier": "^2.0.0",
"ts-jest": "^27.1.3",
"ts-loader": "^9.3.1",
"typescript": "4.6.4",
"webpack-bundle-analyzer": "^4.6.1"
},
"engines": {

View file

@ -1,7 +1,7 @@
import React, { useEffect, useMemo } from 'react';
import { AppRootProps } from '@grafana/data';
import { Button, HorizontalGroup, LinkButton, VerticalGroup } from '@grafana/ui';
import { Button, HorizontalGroup, LinkButton } from '@grafana/ui';
import dayjs from 'dayjs';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
@ -30,8 +30,6 @@ dayjs.extend(isSameOrBefore);
dayjs.extend(isSameOrAfter);
dayjs.extend(isoWeek);
// dayjs().weekday(0);
import './style/vars.css';
import './style/index.css';

View file

@ -3,6 +3,7 @@
Developer-Friendly
Alert Management
with Brilliant Slack Integration
- Connect monitoring systems
- Collect and analyze data
- On-call rotation
@ -10,5 +11,6 @@ with Brilliant Slack Integration
- Never miss alerts with calls and SMS
## Documentation
- [On Github](http://github.com/grafana/oncall)
- [Grafana OnCall](https://grafana.com/docs/grafana-cloud/oncall/)

View file

@ -10,7 +10,3 @@ export function getLabelFromTemplateName(templateName: string, group: any) {
}
return arrayWithNeededValues.join(' ');
}
export function includeTemplateGroup(groupName: string) {
return true;
}

View file

@ -8,8 +8,7 @@ import cn from 'classnames/bind';
import { omit } from 'lodash-es';
import { templatesToRender, Template } from 'components/AlertTemplates/AlertTemplatesForm.config';
import { getLabelFromTemplateName, includeTemplateGroup } from 'components/AlertTemplates/AlertTemplatesForm.helper';
import Collapse from 'components/Collapse/Collapse';
import { getLabelFromTemplateName } from 'components/AlertTemplates/AlertTemplatesForm.helper';
import Block from 'components/GBlock/Block';
import MonacoJinja2Editor from 'components/MonacoJinja2Editor/MonacoJinja2Editor';
import SourceCode from 'components/SourceCode/SourceCode';
@ -41,7 +40,6 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
const {
onUpdateTemplates,
templates,
errors,
alertReceiveChannelId,
alertGroupId,
demoAlertEnabled,
@ -53,6 +51,8 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
const [tempValues, setTempValues] = useState<{
[key: string]: string | null;
}>({});
const [activeGroup, setActiveGroup] = useState<string>();
const [activeTemplate, setActiveTemplate] = useState<Template>();
useEffect(() => {
makeRequest('/preview_template_options/', {});
@ -80,14 +80,11 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
const handleReset = () => {
const temValuesCopy = omit(
tempValues,
groups[activeGroup].map((group: any) => group.name)
groups[activeGroup].map((group) => group.name)
);
setTempValues(temValuesCopy);
};
const [activeGroup, setActiveGroup] = useState<string>();
const [activeTemplate, setActiveTemplate] = useState<any>();
const filteredTemplatesToRender = useMemo(() => {
return templates
? templatesToRender.filter((template) => {
@ -97,13 +94,10 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
}, [templates]);
const groups = useMemo(() => {
const groups: { [key: string]: any } = {};
const groups: { [key: string]: Template[] } = {};
filteredTemplatesToRender.forEach((templateToRender) => {
if (!groups[templateToRender.group]) {
if (!includeTemplateGroup(templateToRender.group)) {
return;
}
groups[templateToRender.group] = [];
}
groups[templateToRender.group].push(templateToRender);
@ -113,11 +107,7 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
const getGroupByTemplateName = (templateName: string) => {
Object.values(groups).find((group) => {
const foundTemplate = group.find((obj: any) => {
if (obj.name == templateName) {
return obj;
}
});
const foundTemplate = group.find((obj) => obj.name === templateName);
setActiveGroup(foundTemplate?.group);
});
};
@ -210,18 +200,18 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
suggestions
</p>
</Text>
{groups[activeGroup].map((activeTemplate: any) => (
{groups[activeGroup].map((activeTemplate) => (
<div
key={activeTemplate.name}
className={cx('template-form', {
'template-form-full': true,
'autoresolve-condition': selectedTemplateName && activeTemplate.name == 'resolve_condition_template',
'autoresolve-condition': selectedTemplateName && activeTemplate.name === 'resolve_condition_template',
})}
>
<Label className={cx({ 'autoresolve-label': activeTemplate.name == 'resolve_condition_template' })}>
<Label className={cx({ 'autoresolve-label': activeTemplate.name === 'resolve_condition_template' })}>
{getLabelFromTemplateName(activeTemplate.name, activeGroup)}
</Label>
{activeTemplate.name == 'resolve_condition_template' && (
{activeTemplate.name === 'resolve_condition_template' && (
<Text type="secondary" size="small">
To activate autoresolving change integration
<Button fill="text" size="sm" onClick={handleGoToTemplateSettingsCllick}>
@ -240,7 +230,7 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
<Text type="secondary">
Press <Text keyboard>Ctrl</Text>+<Text keyboard>Space</Text> to get suggestions
</Text>
{activeGroup === 'web' && activeTemplate.name == 'web_title_template' && (
{activeGroup === 'web' && activeTemplate.name === 'web_title_template' && (
<div className={cx('web-title-message')}>
<Text type="secondary" size="small">
Please note that after changing the web title template new alert groups will be searchable by
@ -271,7 +261,7 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => {
<VerticalGroup>
<Label>{`${capitalCase(activeGroup)} Preview`}</Label>
<VerticalGroup style={{ width: '100%' }}>
{groups[activeGroup].map((template: any) => (
{groups[activeGroup].map((template) => (
<TemplatePreview
active={template.name === activeTemplate?.name}
key={template.name}

View file

@ -1,7 +1,7 @@
import React from 'react';
import { describe, expect, test } from '@jest/globals';
import { render, fireEvent, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import Avatar from 'components/Avatar/Avatar';

View file

@ -19,7 +19,7 @@
.root_selected::before {
display: block;
content: "";
content: '';
position: absolute;
left: 0;
top: 0;

View file

@ -4,7 +4,6 @@ import React from 'react';
import { describe, expect, test } from '@jest/globals';
import { fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import CardButton from 'components/CardButton/CardButton';
@ -23,11 +22,11 @@ describe('CardButton', () => {
const onClickMock = jest.fn();
render(<CardButton {...getProps(onClickMock)} />);
const rootEl = getRootBlockEl()
const rootEl = getRootBlockEl();
fireEvent.click(rootEl);
expect(rootEl.classList).toContain("root_selected")
expect(rootEl.classList).toContain('root_selected');
expect(onClickMock).toHaveBeenCalled();
});

View file

@ -26,7 +26,12 @@ const CardButton: FC<CardButtonProps> = (props) => {
}, [selected]);
return (
<Block onClick={handleClick} withBackground className={cx('root', { root_selected: selected })} data-testid='test__cardButton'>
<Block
onClick={handleClick}
withBackground
className={cx('root', { root_selected: selected })}
data-testid="test__cardButton"
>
<div className={cx('icon')}>{icon}</div>
<div className={cx('meta')}>
<VerticalGroup spacing="xs">

View file

@ -13,8 +13,8 @@ describe('Collapse', () => {
return {
label: 'Toggle',
isOpen: isOpen,
onClick: onClick
} as CollapseProps
onClick: onClick,
} as CollapseProps;
}
test('Content becomes visible on click', () => {
@ -34,7 +34,7 @@ describe('Collapse', () => {
const content = getChildrenEl();
expect(content).toBeNull();
})
});
test('Content is not collapsed for [isOpen=true]', () => {
render(<Collapse {...getProps(true)} />);

View file

@ -1,3 +0,0 @@
.root {
display: block;
}

View file

@ -2,12 +2,9 @@ import React, { FC, useCallback, useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { Button, HorizontalGroup, Icon, Select } from '@grafana/ui';
import cn from 'classnames/bind';
import Text from 'components/Text/Text';
import styles from './CursorPagination.module.css';
interface CursorPaginationProps {
current: string;
onChange: (cursor: string, direction: 'prev' | 'next') => void;
@ -18,8 +15,6 @@ interface CursorPaginationProps {
next: string;
}
const cx = cn.bind(styles);
const CursorPagination: FC<CursorPaginationProps> = (props) => {
const { current, onChange, prev, next, itemsPerPage, itemsPerPageOptions, onChangeItemsPerPage } = props;

View file

@ -1,6 +1,6 @@
import React, { ChangeEvent, FC, useCallback } from 'react';
import { Icon, Input, Button, IconButton } from '@grafana/ui';
import { Icon, Input, IconButton } from '@grafana/ui';
import cn from 'classnames/bind';
import styles from './EscalationsFilters.module.css';

View file

@ -1,3 +0,0 @@
.root {
display: block;
}

View file

@ -2,18 +2,14 @@ import React, { useCallback } from 'react';
import { Field, Form, Input, InputControl, Select, Switch, TextArea } from '@grafana/ui';
import { capitalCase } from 'change-case';
import cn from 'classnames/bind';
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
import GSelect from 'containers/GSelect/GSelect';
import RemoteSelect from 'containers/RemoteSelect/RemoteSelect';
import styles from './GForm.module.css';
interface GFormProps {
form: { name: string; fields: FormItem[] };
data: any;
/* errors: { [key: string]: string }; */
onSubmit: (data: any) => void;
}
@ -21,8 +17,6 @@ const nullNormalizer = (value: string) => {
return value || null;
};
const cx = cn.bind(styles);
function renderFormControl(formItem: FormItem, register: any, control: any) {
switch (formItem.type) {
case FormItemType.Input:
@ -99,20 +93,18 @@ const GForm = (props: GFormProps) => {
return (
<Form maxWidth="none" id={form.name} defaultValues={data} onSubmit={handleSubmit}>
{({ register, errors, control }) => {
return form.fields.map((formItem: FormItem, formIndex: number) => {
return (
<Field
key={formIndex}
disabled={formItem.getDisabled ? formItem.getDisabled(data) : false}
label={formItem.label || capitalCase(formItem.name)}
invalid={!!errors[formItem.name]}
error={`${capitalCase(formItem.name)} is required`}
description={formItem.description}
>
{renderFormControl(formItem, register, control)}
</Field>
);
});
return form.fields.map((formItem: FormItem, formIndex: number) => (
<Field
key={formIndex}
disabled={formItem.getDisabled ? formItem.getDisabled(data) : false}
label={formItem.label || capitalCase(formItem.name)}
invalid={!!errors[formItem.name]}
error={`${capitalCase(formItem.name)} is required`}
description={formItem.description}
>
{renderFormControl(formItem, register, control)}
</Field>
));
}}
</Form>
);

View file

@ -1,5 +1,3 @@
import { Moment } from 'moment';
export enum FormItemType {
'Input' = 'input',
'TextArea' = 'textarea',
@ -7,13 +5,6 @@ export enum FormItemType {
'GSelect' = 'gselect',
'Switch' = 'switch',
'RemoteSelect' = 'remoteselect',
/* 'InputNumber' = 'input-number',
'Select' = 'select',
'Switch' = 'switch',
'ASelect' = 'aselect',
'JustSelect' = 'just-select',
'DatePicker' = 'datepicker', */
}
export interface FormItem {

View file

@ -1,4 +1,4 @@
import React, { FC, useState, useCallback, useRef, useEffect } from 'react';
import React, { useCallback, useRef, useEffect } from 'react';
import { LoadingPlaceholder } from '@grafana/ui';
import cn from 'classnames/bind';
@ -37,7 +37,6 @@ const GList = <T extends WithId>(props: GListProps<T>) => {
const divToScroll = selectedElement.parentElement.parentElement;
const maxScroll = Math.max(0, selectedElement.parentElement.offsetHeight - divToScroll.offsetHeight);
const minScroll = 0;
const scrollTop =
selectedElement.offsetTop -

View file

@ -1,4 +1,4 @@
import React, { FC, useState, useCallback, useMemo, ChangeEvent } from 'react';
import React, { FC, useCallback, useMemo, ChangeEvent } from 'react';
import { Pagination, Checkbox, Icon } from '@grafana/ui';
import cn from 'classnames/bind';
@ -96,7 +96,7 @@ const GTable: FC<Props> = (props) => {
const handleMasterCheckboxChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const { selectedRowKeys, onChange } = rowSelection;
const { onChange } = rowSelection;
if (event.target.checked) {
const newRowSelection = data.map((item: any) => item[rowKey as string]);
onChange(newRowSelection);

View file

@ -39,8 +39,7 @@ const Modal: FC<PropsWithChildren<ModalProps>> = (props) => {
contentLabel={title}
className={cx('root')}
overlayClassName={cx('overlay')}
overlayElement={(props, contentElement) => contentElement} // render without overlay to allow body scroll
/* bodyOpenClassName={cx('body-open')} */
overlayElement={(_props, contentElement) => contentElement} // render without overlay to allow body scroll
contentElement={contentElement}
>
{children}

View file

@ -1,3 +0,0 @@
.root {
display: block;
}

View file

@ -1,14 +1,11 @@
import React, { FC, useCallback, useMemo, useRef, useEffect } from 'react';
import React, { FC, useCallback } from 'react';
import { CodeEditor, CodeEditorSuggestionItemKind, LoadingPlaceholder } from '@grafana/ui';
import cn from 'classnames/bind';
import { getPaths } from 'utils';
import { conf, language } from './jinja2';
import styles from './MonacoJinja2Editor.module.css';
declare const monaco: any;
interface MonacoJinja2EditorProps {
@ -19,8 +16,6 @@ interface MonacoJinja2EditorProps {
loading: boolean;
}
const cx = cn.bind(styles);
const PREDEFINED_TERMS = [
'grafana_oncall_link',
'integration_name',

View file

@ -1,6 +1,5 @@
import React, { FC, useCallback, useState } from 'react';
import { getLocationSrv } from '@grafana/runtime';
import { Button, Drawer, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
@ -39,10 +38,6 @@ const NewScheduleSelector: FC<NewScheduleSelectorProps> = (props) => {
<Drawer scrollableContent title="Create new schedule" onClose={onHide} closeOnMaskClick>
<div className={cx('content')}>
<VerticalGroup spacing="lg">
{/*<Text type="secondary">
Manage on-call schedules using your favourite calendar app, such as Google Calendar or Microsoft Outlook. To
schedule on-call shifts create a new calendar and use events with the teammates usernames
</Text>*/}
<Block bordered withBackground className={cx('block')}>
<HorizontalGroup justify="space-between">
<HorizontalGroup spacing="md">

View file

@ -2,7 +2,6 @@ import React, { useEffect } from 'react';
import { Button, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import { PropTypes } from 'mobx-react';
import PluginLink from 'components/PluginLink/PluginLink';
import Text from 'components/Text/Text';

View file

@ -1,7 +1,7 @@
import React, { ChangeEvent } from 'react';
import { SelectableValue } from '@grafana/data';
import { Button, Input, Select, Tooltip, IconButton } from '@grafana/ui';
import { Button, Input, Select, IconButton } from '@grafana/ui';
import cn from 'classnames/bind';
import moment from 'moment-timezone';
import { SortableElement } from 'react-sortable-hoc';
@ -11,7 +11,6 @@ import PluginLink from 'components/PluginLink/PluginLink';
import TimeRange from 'components/TimeRange/TimeRange';
import Timeline from 'components/Timeline/Timeline';
import GSelect from 'containers/GSelect/GSelect';
import RemoteSelect from 'containers/RemoteSelect/RemoteSelect';
import UserTooltip from 'containers/UserTooltip/UserTooltip';
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
import { prepareEscalationPolicy } from 'models/escalation_policy/escalation_policy.helpers';
@ -147,7 +146,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
className={cx('select', 'control', 'multiSelect')}
value={notify_to_users_queue}
onChange={this._getOnChangeHandler('notify_to_users_queue')}
getOptionLabel={({ label, value }: SelectableValue) => <UserTooltip id={value} />}
getOptionLabel={({ value }: SelectableValue) => <UserTooltip id={value} />}
/>
</WithPermissionControl>
);

View file

@ -1,15 +1,9 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import { FadeTransition } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { CSSTransition } from 'react-transition-group';
import styles from './ScheduleBorderedAvatar.module.scss';
import animationStyles from 'containers/Rotations/Rotations.module.css';
const cx = cn.bind(styles);
interface ScheduleBorderedAvatarProps {
@ -52,7 +46,9 @@ export default function ScheduleBorderedAvatar({
}
function renderColorPaths(colors: string[]) {
if (!colors?.length) {return null;}
if (!colors?.length) {
return null;
}
const colorSchemeList = colors;
if (colors.length === 1) {
@ -75,7 +71,7 @@ export default function ScheduleBorderedAvatar({
lastX = x;
lastY = y;
return <path d={d} stroke={colors[colorIndex]} />;
return <path key={colorIndex} d={d} stroke={colors[colorIndex]} />;
});
}
}

View file

@ -28,15 +28,3 @@
.tooltip {
width: auto;
}
/*
.tooltip__type_link {
border: 1px solid #6CCF8E;
background: #132322;
}
.tooltip__type_warning {
border: 1px solid #F8D06B;
background: #3A301E;
}
*/

View file

@ -1,6 +1,6 @@
import React, { FC, useCallback } from 'react';
import React, { FC } from 'react';
import { HorizontalGroup, VerticalGroup, Icon, IconButton, Tooltip, IconName } from '@grafana/ui';
import { HorizontalGroup, VerticalGroup, Icon, Tooltip, IconName } from '@grafana/ui';
import cn from 'classnames/bind';
import Text, { TextType } from 'components/Text/Text';
@ -25,16 +25,6 @@ const typeToColor = {
warning: 'warning',
};
const typeToBorderColor = {
link: '#6CCF8E',
warning: '#F8D06B',
};
const typeToBackgroundColor = {
link: '#132322',
warning: '#3A301E',
};
const cx = cn.bind(styles);
const ScheduleCounter: FC<ScheduleCounterProps> = (props) => {

View file

@ -25,7 +25,7 @@
.hr {
width: 100%;
margin: 0 -11px;
margin: 0 -11px;
}
.times {

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { Icon, Button, HorizontalGroup, VerticalGroup } from '@grafana/ui';
import { HorizontalGroup, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
@ -9,8 +9,6 @@ import Text from 'components/Text/Text';
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
import { User } from 'models/user/user.types';
import Line from './img/line.svg';
import styles from './ScheduleUserDetails.module.css';
interface ScheduleUserDetailsProps {
@ -20,30 +18,9 @@ interface ScheduleUserDetailsProps {
const cx = cn.bind(styles);
enum UserOncallStatus {
Now = 'now',
Outside = 'outside',
Inside = 'inside',
}
const userOncallStatusToText = {
[UserOncallStatus.Now]: 'Oncall now',
[UserOncallStatus.Inside]: 'Inside working hours',
[UserOncallStatus.Outside]: 'Outside working hours',
};
const ScheduleUserDetails: FC<ScheduleUserDetailsProps> = (props) => {
const { user, currentMoment } = props;
const userStatus =
Math.random() > 0.66
? UserOncallStatus.Now
: Math.random() > 0.33
? UserOncallStatus.Inside
: UserOncallStatus.Outside;
const userMoment = currentMoment.tz(user.timezone);
const userOffsetHoursStr = getTzOffsetString(userMoment);
return (
@ -51,67 +28,12 @@ const ScheduleUserDetails: FC<ScheduleUserDetailsProps> = (props) => {
<VerticalGroup spacing="sm">
<HorizontalGroup justify="space-between">
<Avatar src={user.avatar} size="large" />
{/*<Button variant="secondary">
<HorizontalGroup spacing="sm">
<Icon name="bell" />
Push
</HorizontalGroup>
</Button>*/}
</HorizontalGroup>
<VerticalGroup spacing="sm">
<Text type="primary">{user.username}</Text>
<Text type="secondary">
{`${userMoment.tz(user.timezone).format('DD MMM, HH:mm')}`} {userOffsetHoursStr}
</Text>
{/* <div
className={cx('oncall-badge', {
[`oncall-badge__type_${userStatus}`]: true,
})}
>
{userOncallStatusToText[userStatus]}
</div>
<HorizontalGroup>
<VerticalGroup spacing="sm">
<Text type="primary">Next shift</Text>
<div className={cx('times')}>
<HorizontalGroup>
<img src={Line} />
<VerticalGroup spacing="none">
<Text type="secondary">30 apr, 00:00</Text>
<Text type="secondary">30 apr, 23:59</Text>
</VerticalGroup>
</HorizontalGroup>
</div>
</VerticalGroup>
<VerticalGroup spacing="sm">
<Text type="primary">Last shift</Text>
<div className={cx('times')}>
<HorizontalGroup>
<img src={Line} />
<VerticalGroup spacing="none">
<Text type="secondary">30 apr, 00:00</Text>
<Text type="secondary">30 apr, 23:59</Text>
</VerticalGroup>
</HorizontalGroup>
</div>
</VerticalGroup>
</HorizontalGroup>
</VerticalGroup>
<hr style={{ width: '100%' }} />
<VerticalGroup spacing="sm">
<Text type="primary">Contacts</Text>
<HorizontalGroup spacing="sm">
<Icon className={cx('icon')} name="message" />
<Text type="link">mail@grafana.com</Text>
</HorizontalGroup>
<HorizontalGroup spacing="sm">
<Icon className={cx('icon')} name="slack" />
<Text type="link">@slackid</Text>
</HorizontalGroup>
<HorizontalGroup spacing="sm">
<Icon className={cx('icon')} name="phone" />
<Text type="secondary">+39 555 449 00 00</Text>
</HorizontalGroup>*/}
</VerticalGroup>
</VerticalGroup>
</div>

View file

@ -1,4 +1,4 @@
import moment from 'moment';
import moment from 'moment-timezone';
export function optionToDateString(option: string) {
switch (option) {

View file

@ -1,8 +1,8 @@
import React, { ChangeEvent, useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { DatePickerWithInput, Field, HorizontalGroup, RadioButtonGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import moment from 'moment';
import moment from 'moment-timezone';
import { dateStringToOption, optionToDateString } from './SchedulesFilters.helpers';
import { SchedulesFiltersType } from './SchedulesFilters.types';
@ -17,18 +17,22 @@ interface SchedulesFiltersProps {
className?: string;
}
const SchedulesFilters = (props: SchedulesFiltersProps) => {
const { value, onChange, className } = props;
const handleDateChange = useCallback((date: Date) => {
onChange({ selectedDate: moment(date).format('YYYY-MM-DD') });
}, []);
const SchedulesFilters = ({ value, onChange, className }: SchedulesFiltersProps) => {
const handleDateChange = useCallback(
(date: Date) => {
onChange({ selectedDate: moment(date).format('YYYY-MM-DD') });
},
[onChange]
);
const option = useMemo(() => dateStringToOption(value.selectedDate), [value]);
const handleOptionChange = useCallback((option: string) => {
onChange({ ...value, selectedDate: optionToDateString(option) });
}, []);
const handleOptionChange = useCallback(
(option: string) => {
onChange({ ...value, selectedDate: optionToDateString(option) });
},
[onChange, value]
);
const datePickerValue = useMemo(() => moment(value.selectedDate).toDate(), [value]);

View file

@ -1,5 +1,3 @@
import { Moment } from 'moment';
export interface SchedulesFiltersType {
selectedDate: string;
}

View file

@ -1,4 +1,4 @@
import moment from 'moment';
import moment from 'moment-timezone';
export function optionToDateString(option: string) {
switch (option) {

View file

@ -1,11 +1,10 @@
import React, { ChangeEvent, useCallback, useMemo, useState } from 'react';
import React, { ChangeEvent, useCallback } from 'react';
import { DatePickerWithInput, Field, HorizontalGroup, Icon, Input, RadioButtonGroup } from '@grafana/ui';
import { Field, HorizontalGroup, Icon, Input, RadioButtonGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import { ScheduleType } from 'models/schedule/schedule.types';
import { dateStringToOption, optionToDateString } from './SchedulesFilters.helpers';
import { SchedulesFiltersType } from './SchedulesFilters.types';
import styles from './SchedulesFilters.module.css';

View file

@ -26,4 +26,4 @@
}
.copyButton {
opacity: 0;
}
}

View file

@ -2,8 +2,7 @@ import 'jest/matchMedia.ts';
import React from 'react';
import { describe, expect, test } from '@jest/globals';
import { render, fireEvent, screen } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import SourceCode from './SourceCode';

View file

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { Button, Icon, IconButton } from '@grafana/ui';
import { Button, IconButton } from '@grafana/ui';
import cn from 'classnames/bind';
import CopyToClipboard from 'react-copy-to-clipboard';
@ -33,7 +33,13 @@ const SourceCode: FC<SourceCodeProps> = (props) => {
{showClipboardIconOnly ? (
<IconButton className={cx('copyIcon')} size={'lg'} name="copy" data-testid="test__copyIcon" />
) : (
<Button className={cx('copyButton')} variant="primary" size="xs" icon="copy" data-testid="test__copyIconWithText">
<Button
className={cx('copyButton')}
variant="primary"
size="xs"
icon="copy"
data-testid="test__copyIconWithText"
>
Copy
</Button>
)}

View file

@ -1,6 +1,6 @@
import React, { FC, useState, useCallback, useMemo, ChangeEvent } from 'react';
import React, { FC, useMemo } from 'react';
import { Pagination, Checkbox, Icon, VerticalGroup } from '@grafana/ui';
import { Pagination, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import Table from 'rc-table';
import { TableProps } from 'rc-table/lib/Table';
@ -34,21 +34,20 @@ export interface Props<RecordType = unknown> extends TableProps<RecordType> {
const GTable: FC<Props> = (props) => {
const { columns, data, className, pagination, loading, rowKey, expandable, ...restProps } = props;
const { page, total: numberOfPages, onChange: onNavigate } = pagination || {};
const expandableFn = useMemo(() => {
return expandable
? {
...expandable,
expandIcon: ({ expanded, record }) => {
expandIcon: ({ expanded }) => {
return (
<div className={cx('expand-icon', { [`expand-icon__expanded`]: expanded })}>
<ExpandIcon />
</div>
);
},
expandedRowClassName: (record, index) => (index % 2 === 0 ? cx('row-even') : cx('row-odd')),
expandedRowClassName: (_record, index) => (index % 2 === 0 ? cx('row-even') : cx('row-odd')),
}
: null;
}, [expandable]);
@ -61,7 +60,7 @@ const GTable: FC<Props> = (props) => {
columns={columns}
data={data}
expandable={expandableFn}
rowClassName={(record, index) => (index % 2 === 0 ? cx('row-even') : cx('row-odd'))}
rowClassName={(_record, index) => (index % 2 === 0 ? cx('row-even') : cx('row-odd'))}
{...restProps}
/>
{pagination && (

View file

@ -40,7 +40,6 @@
white-space: nowrap;
}
.keyboard {
margin: 0 0.2em;
padding: 0.15em 0.4em 0.1em;

View file

@ -69,9 +69,8 @@ const Text: TextInterface = (props) => {
const handleConfirmEdit = useCallback(() => {
setIsEditMode(false);
onTextChange(value);
}, [value]);
}, [value, onTextChange]);
const handleInputChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);

View file

@ -1,4 +1,4 @@
import React, { FC } from 'react';
import React from 'react';
import cn from 'classnames/bind';

View file

@ -1,4 +1,4 @@
import React, { FC } from 'react';
import React from 'react';
import cn from 'classnames/bind';
@ -15,21 +15,19 @@ export interface TimelineItemProps {
children?: any;
}
const TimelineItem: React.FC<TimelineItemProps> = (props) => {
const { className, contentClassName, children, color = '#3274D9', number } = props;
const style = { backgroundColor: color };
return (
<li className={cx('item', className)}>
{/*<Badge count={badge} style={style} showZero={false}>*/}
<div className={cx('dot')} style={{ backgroundColor: color }}>
{number}
</div>
{/*</Badge>*/}
<div className={cx('content', contentClassName)}>{children}</div>
</li>
);
};
const TimelineItem: React.FC<TimelineItemProps> = ({
className,
contentClassName,
children,
color = '#3274D9',
number,
}) => (
<li className={cx('item', className)}>
<div className={cx('dot')} style={{ backgroundColor: color }}>
{number}
</div>
<div className={cx('content', contentClassName)}>{children}</div>
</li>
);
export default TimelineItem;

View file

@ -49,8 +49,9 @@ const TimelineMarks: FC<TimelineMarksProps> = (props) => {
<div className={cx('root')}>
{debug && (
<svg version="1.1" width="100%" height="6px" xmlns="http://www.w3.org/2000/svg" className={cx('debug-scale')}>
{cuts.map((cut, index) => (
{cuts.map((_cut, index) => (
<line
key={index}
x1={`${(index * 100) / (24 * 7)}%`}
strokeWidth={1}
y1="0"

View file

@ -1,6 +1,5 @@
import React, { FC } from 'react';
import { Button, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import Block from 'components/GBlock/Block';
@ -13,7 +12,6 @@ import scheduleIcon from './icons/calendar-icon.svg';
import chatIcon from './icons/chat-icon.svg';
import escalationIcon from './icons/escalation-icon.svg';
import integrationsIcon from './icons/integration-icon.svg';
import arrowIcon from './icons/long-arrow.svg';
import styles from './Tutorial.module.css';

View file

@ -2,7 +2,7 @@ import { Item } from './UserGroups.types';
export const toPlainArray = (groups: string[][]) => {
const items: Item[] = [];
groups.forEach((group: string[], groupIndex: number) => {
groups.forEach((_group: string[], groupIndex: number) => {
items.push({
key: `group-${groupIndex}`,
type: 'group',

View file

@ -24,7 +24,7 @@
.separator::before {
display: block;
content: "";
content: '';
flex-grow: 1;
border-bottom: var(--border-medium);
height: 0;
@ -33,7 +33,7 @@
.separator::after {
display: block;
content: "";
content: '';
flex-grow: 1;
border-bottom: var(--border-medium);
height: 0;

View file

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { VerticalGroup, HorizontalGroup, IconButton, Field, Input } from '@grafana/ui';
import { VerticalGroup, HorizontalGroup, IconButton } from '@grafana/ui';
import { arrayMoveImmutable } from 'array-move';
import cn from 'classnames/bind';
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
@ -45,7 +45,7 @@ const UserGroups = (props: UserGroupsProps) => {
k++;
if (k === index) {
newGroups[i] = newGroups[i].filter((item, itemIndex) => itemIndex !== j);
newGroups[i] = newGroups[i].filter((_item, itemIndex) => itemIndex !== j);
onChange(newGroups.filter((group) => group.length));
return;
}

View file

@ -3,7 +3,6 @@ import React, { FC, useCallback, useMemo } from 'react';
import { Select } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { get } from 'lodash-es';
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
import { Timezone } from 'models/timezone/timezone.types';

View file

@ -70,6 +70,7 @@ const UsersFilters = (props: UsersFiltersProps) => {
<HorizontalGroup>
{roleOptions.map((option) => (
<Checkbox
key={option.value}
value={value.roles.includes(option.value)}
label={option.label}
onChange={onChangeRolesCallback(option.value)}

View file

@ -4,14 +4,22 @@ import cn from 'classnames/bind';
import styles from './VerticalTabsBar.module.css';
const cx = cn.bind(styles);
interface TabProps {
id: string;
children?: any;
}
export const VerticalTab: FC<TabProps> = ({ children }) => {
return <>{children}</>;
};
interface VerticalTabsBarProps {
children: Array<React.ReactElement<TabProps>> | React.ReactElement<TabProps>;
activeTab: string;
onChange: (id: string) => void;
}
const cx = cn.bind(styles);
const VerticalTabsBar = (props: VerticalTabsBarProps) => {
const { children, activeTab, onChange } = props;
@ -25,8 +33,9 @@ const VerticalTabsBar = (props: VerticalTabsBarProps) => {
<div className={cx('root')}>
{React.Children.toArray(children)
.filter(Boolean)
.map((child: React.ReactElement) => (
.map((child: React.ReactElement, idx) => (
<div
key={idx}
onClick={getClickHandler(child.props.id)}
className={cx('tab', { tab_active: activeTab === child.props.id })}
>
@ -38,12 +47,3 @@ const VerticalTabsBar = (props: VerticalTabsBarProps) => {
};
export default VerticalTabsBar;
interface TabProps {
id: string;
children?: any;
}
export const VerticalTab: FC<TabProps> = ({ children }) => {
return <>{children}</>;
};

View file

@ -1,3 +0,0 @@
.root {
display: block;
}

View file

@ -1,9 +1,6 @@
import React, { FC, ReactElement, useCallback, useState } from 'react';
import React, { ReactElement, useCallback, useState } from 'react';
import { ConfirmModal } from '@grafana/ui';
import cn from 'classnames/bind';
import styles from './WithConfirm.module.css';
interface WithConfirmProps {
children: ReactElement;
@ -13,8 +10,6 @@ interface WithConfirmProps {
disabled?: boolean;
}
const cx = cn.bind(styles);
const WithConfirm = (props: WithConfirmProps) => {
const { children, title = 'Are you sure to delete?', body, confirmText = 'Delete', disabled } = props;

View file

@ -71,7 +71,7 @@ export const getNonWorkingMoments = (startMoment, endMoment, workingHours) => {
const nonWorkingMoments = [{ start: startMoment, end: endMoment }];
let lastNonWorkingRange = nonWorkingMoments[0];
for (const [i, range] of workingHours.entries()) {
for (const [_i, range] of workingHours.entries()) {
lastNonWorkingRange.end = range.start;
lastNonWorkingRange = { start: range.end, end: undefined };

View file

@ -2,7 +2,6 @@ import React, { FC, useMemo } from 'react';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import localeData from 'dayjs/plugin/localeData';
import { Timezone } from 'models/timezone/timezone.types';
@ -11,8 +10,6 @@ import { getNonWorkingMoments, getWorkingMoments } from './WorkingHours.helpers'
import styles from './WorkingHours.module.css';
import { start } from 'repl';
interface WorkingHoursProps {
timezone: Timezone;
workingHours: any;
@ -34,33 +31,11 @@ const WorkingHours: FC<WorkingHoursProps> = (props) => {
[startMoment, endMoment, workingHours, timezone]
);
/*console.log(
workingMoments.map(({ start, end }) => `${start.diff(startMoment, 'hours')} - ${end.diff(startMoment, 'hours')}`)
);*/
const nonWorkingMoments = useMemo(
() => getNonWorkingMoments(startMoment, endMoment, workingMoments),
[startMoment, endMoment, workingMoments]
);
// console.log(startMoment, startMoment.toString());
/* console.log(
workingMoments.map(
(range) =>
`${range.start.tz(timezone).format('D MMM ddd HH:ss')} - ${range.end.tz(timezone).format('D MMM ddd HH:ss')}`
)
); */
// console.log(workingHours);
/*console.log(
nonWorkingMoments.map(
(range) =>
`${range.start.tz(timezone).format('D MMM ddd HH:ss')} - ${range.end.tz(timezone).format('D MMM ddd HH:ss')}`
)
);*/
return (
<svg
version="1.1"

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Icon, Tooltip, HorizontalGroup, VerticalGroup } from '@grafana/ui';
import { Tooltip, HorizontalGroup, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import Emoji from 'react-emoji-render';
@ -8,9 +8,8 @@ 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 { GrafanaIcon, HeartGreenIcon, HeartRedIcon } from 'icons';
import { HeartGreenIcon, HeartRedIcon } from 'icons';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { SelectOption } from 'state/types';
import { useStore } from 'state/useStore';
import styles from './AlertReceiveChannelCard.module.css';

View file

@ -34,7 +34,6 @@ import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
import { EscalationPolicyOption } from 'models/escalation_policy/escalation_policy.types';
import { MaintenanceType } from 'models/maintenance/maintenance.types';
import { getSlackChannelName } from 'models/slack_channel/slack_channel.helpers';
import { WithStoreProps } from 'state/types';
import { UserAction } from 'state/userAction';
import { withMobXProviderContext } from 'state/withStore';
@ -63,6 +62,14 @@ interface AlertRulesState {
editIntegrationName?: string;
}
const Notification: React.FC = () => (
<div>
Demo alert was generated. Find it on the
<PluginLink query={{ page: 'incidents' }}> "Alert Groups" </PluginLink>
page and make sure it didn't freak out your colleagues 😉
</div>
);
@observer
class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
state: AlertRulesState = {
@ -77,9 +84,7 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
}
}
componentDidUpdate(prevProps: Readonly<AlertRulesProps>, prevState: Readonly<AlertRulesState>, snapshot?: any) {
const { store } = this.props;
componentDidUpdate(prevProps: Readonly<AlertRulesProps>, _prevState: Readonly<AlertRulesState>, _snapshot?: any) {
if (this.props.alertReceiveChannelId && prevProps.alertReceiveChannelId !== this.props.alertReceiveChannelId) {
if (prevProps.alertReceiveChannelId) {
this.setState({
@ -130,14 +135,12 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
render() {
const {
alertReceiveChannelIdToCreateChannelFilter,
channelFilterToEdit,
settingsVisible,
routeToDelete,
escalationChainIdToCopy,
channelFilterIdToCopyEscalationChain,
editIntegrationName,
} = this.state;
const { store, alertReceiveChannelId, onEditAlertReceiveChannelTemplates, onShowSettings } = this.props;
const { store, alertReceiveChannelId, onShowSettings } = this.props;
const { alertReceiveChannelStore } = store;
const alertReceiveChannel = alertReceiveChannelStore.items[alertReceiveChannelId];
@ -153,7 +156,6 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
<div className={cx('root')}>
<Block className={cx('headerBlock')}>
<div className={cx('header')}>
{/* <HorizontalGroup> */}
<Text.Title level={4}>
<HorizontalGroup>
Escalate
@ -197,7 +199,6 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
</div>
</Modal>
)}
{/* </HorizontalGroup> */}
<div className={cx('buttons')}>
<Button
variant="secondary"
@ -377,7 +378,7 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
handleEscalationChainCreate = async (id: EscalationChain['id']) => {
const { store } = this.props;
const { alertReceiveChannelStore } = store;
const { escalationChainIdToCopy, channelFilterIdToCopyEscalationChain } = this.state;
const { channelFilterIdToCopyEscalationChain } = this.state;
await alertReceiveChannelStore
.saveChannelFilter(channelFilterIdToCopyEscalationChain, { escalation_chain: id })
@ -389,8 +390,7 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
};
handleDeleteAlertReceiveChannel = () => {
const { store, alertReceiveChannelId, onDelete } = this.props;
const { alertReceiveChannelId, onDelete } = this.props;
onDelete(alertReceiveChannelId);
};
@ -460,12 +460,13 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
return (
<div>
{channelFilterIds.map((channelFilterId: ChannelFilter['id'], index: number) => {
{channelFilterIds.map((channelFilterId: ChannelFilter['id']) => {
const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId];
if (channelFilterId === channelFilterToEdit?.id) {
return (
<ChannelFilterForm
key={channelFilterId}
className={cx('route')}
id={channelFilterToEdit.id}
alertReceiveChannelId={channelFilterToEdit.alert_receive_channel}
@ -590,8 +591,6 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
};
getChannelFilterToggleHandler = (channelFilterId: ChannelFilter['id']) => {
const { store } = this.props;
return (isOpen: boolean) => {
const { expandedRoutes } = this.state;
@ -685,17 +684,9 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
channelFilterId: ChannelFilter['id']
) => {
const { store } = this.props;
const { telegramChannelStore, teamStore } = store;
const channelFilterIds = store.alertReceiveChannelStore.channelFilterIds[alertReceiveChannelId];
const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId];
const telegramChannel =
channelFilter.telegram_channel && telegramChannelStore.items[channelFilter.telegram_channel];
const slackChannelName = getSlackChannelName(channelFilter.slack_channel || teamStore.currentTeam?.slack_channel);
const index = channelFilterIds.indexOf(channelFilterId);
return (
<div className={cx('channel-filter-header')}>
@ -760,9 +751,7 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
_renderEscalationPolicies = (channelFilterId: ChannelFilter['id']) => {
const { store } = this.props;
const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId];
const escalationChainId = channelFilter.escalation_chain;
return (
@ -802,13 +791,7 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
return () => {
alertReceiveChannelStore.sendDemoAlert(id).then(() => {
alertReceiveChannelStore.updateCounters();
openNotification(
<div>
Demo alert was generated. Find it on the
<PluginLink query={{ page: 'incidents' }}> "Alert Groups" </PluginLink>
page and make sure it didn't freak out your colleagues 😉
</div>
);
openNotification(<Notification />);
});
};
};
@ -819,13 +802,7 @@ class AlertRules extends React.Component<AlertRulesProps, AlertRulesState> {
} = this.props;
return () => {
alertReceiveChannelStore.sendDemoAlertToParticularRoute(id).then(() => {
openNotification(
<div>
Demo alert was generated. Find it on the
<PluginLink query={{ page: 'incidents' }}> "Alert Groups" </PluginLink>
page and make sure it didn't freak out your colleagues 😉
</div>
);
openNotification(<Notification />);
});
};
};

View file

@ -30,7 +30,7 @@ const SlackConnector = (props: SlackConnectorProps) => {
const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId];
const handleSlackChannelChange = useCallback((value: SlackChannel['id'], slackChannel: SlackChannel) => {
const handleSlackChannelChange = useCallback((_value: SlackChannel['id'], slackChannel: SlackChannel) => {
// @ts-ignore actually slack_channel is just slack_channel_id when saving
alertReceiveChannelStore.saveChannelFilter(channelFilterId, { slack_channel: slackChannel?.slack_id || null });
}, []);

View file

@ -18,15 +18,11 @@ interface TelegramConnectorProps {
channelFilterId: ChannelFilter['id'];
}
const TelegramConnector = (props: TelegramConnectorProps) => {
const { channelFilterId } = props;
const TelegramConnector = ({ channelFilterId }: TelegramConnectorProps) => {
const { alertReceiveChannelStore } = useStore();
const channelFilter = alertReceiveChannelStore.channelFilters[channelFilterId];
const store = useStore();
const { teamStore, alertReceiveChannelStore } = store;
const channelFilter = store.alertReceiveChannelStore.channelFilters[channelFilterId];
const handleTelegramChannelChange = useCallback((value: TelegramChannel['id'], telegramChannel: TelegramChannel) => {
const handleTelegramChannelChange = useCallback((_value: TelegramChannel['id'], telegramChannel: TelegramChannel) => {
alertReceiveChannelStore.saveChannelFilter(channelFilterId, { telegram_channel: telegramChannel?.id || null });
}, []);

View file

@ -1,3 +0,0 @@
.root {
display: block;
}

View file

@ -1,7 +1,6 @@
import React from 'react';
import { VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import Timeline from 'components/Timeline/Timeline';
import SlackConnector from 'containers/AlertRules/parts/connectors/SlackConnector';
@ -9,10 +8,6 @@ import TelegramConnector from 'containers/AlertRules/parts/connectors/TelegramCo
import { ChannelFilter } from 'models/channel_filter';
import { useStore } from 'state/useStore';
import styles from 'containers/AlertRules/parts/index.module.css';
const cx = cn.bind(styles);
interface ChatOpsConnectorsProps {
channelFilterId: ChannelFilter['id'];
}

View file

@ -1,12 +1,10 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Button } from '@grafana/ui';
import { observer } from 'mobx-react';
import AlertTemplatesForm from 'components/AlertTemplates/AlertTemplatesForm';
import { AlertReceiveChannel } from 'models/alert_receive_channel';
import { Alert } from 'models/alertgroup/alertgroup.types';
import { RootStore } from 'state';
import { useStore } from 'state/useStore';
import { openNotification } from 'utils';

View file

@ -10,7 +10,6 @@ import SourceCode from 'components/SourceCode/SourceCode';
import { ApiToken } from 'models/api_token/api_token.types';
import { useStore } from 'state/useStore';
import { openErrorNotification, openNotification } from 'utils';
import { getItem } from 'utils/localStorage';
import styles from './ApiTokenForm.module.css';

View file

@ -3,7 +3,7 @@ import React from 'react';
import { Button, HorizontalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import moment from 'moment';
import moment from 'moment-timezone';
import GTable from 'components/GTable/GTable';
import Text from 'components/Text/Text';
@ -74,13 +74,6 @@ class ApiTokens extends React.Component<ApiTokensProps, any> {
<div className={cx('header')}>
<HorizontalGroup align="flex-end">
<Text.Title level={3}>API Tokens</Text.Title>
{/*<a target="_blank" href="https://a-03-dev-us-central-0.grafana.net/api-docs/#introduction">
API Docs
</a>
<Text type="secondary">|</Text>
<a target="_blank" href="https://github.com/grafana/amixr/tree/dev/docs/terraform-provider">
Terraform Docs
</a>*/}
</HorizontalGroup>
<WithPermissionControl userAction={UserAction.UpdateApiTokens}>
<Button

View file

@ -4,13 +4,11 @@ import { SelectableValue } from '@grafana/data';
import { Button, Field, HorizontalGroup, Icon, Modal } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import moment from 'moment';
import Emoji from 'react-emoji-render';
import moment from 'moment-timezone';
import Text from 'components/Text/Text';
import GSelect from 'containers/GSelect/GSelect';
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
import { AlertGroupStore } from 'models/alertgroup/alertgroup';
import { Alert } from 'models/alertgroup/alertgroup.types';
import { useStore } from 'state/useStore';
import { UserAction } from 'state/userAction';
@ -25,9 +23,11 @@ interface AttachIncidentFormProps {
onHide: () => void;
}
const AttachIncidentForm = observer((props: AttachIncidentFormProps) => {
const { id, onUpdate, onHide } = props;
interface GroupedAlertNumberProps {
value: Alert['pk'];
}
const AttachIncidentForm = observer(({ id, onUpdate, onHide }: AttachIncidentFormProps) => {
const store = useStore();
const { alertGroupStore } = store;
@ -43,17 +43,10 @@ const AttachIncidentForm = observer((props: AttachIncidentFormProps) => {
onHide();
onUpdate();
});
}, [selected]);
}, [selected, alertGroupStore, id, onHide, onUpdate]);
interface GroupedAlertNumberProps {
value: Alert['pk'];
}
const GroupedAlertNumber = observer((props: GroupedAlertNumberProps) => {
const store = useStore();
const { value } = props;
const { alertGroupStore } = store;
const GroupedAlertNumber = observer(({ value }: GroupedAlertNumberProps) => {
const { alertGroupStore } = useStore();
const alert = alertGroupStore.items[value];
return (
@ -88,7 +81,7 @@ const AttachIncidentForm = observer((props: AttachIncidentFormProps) => {
displayField="render_for_web.title"
placeholder="Select Incident"
className={cx('select', 'control')}
filterOptions={(id) => id !== props.id}
filterOptions={(optionId) => optionId !== id}
value={selected}
onChange={getChangeHandler}
getDescription={(item: Alert) => moment(item.started_at).format('MMM DD, YYYY hh:mm A')}

View file

@ -1,6 +1,6 @@
import React, { ChangeEvent, useCallback, useState } from 'react';
import { Card, EmptySearchResult, HorizontalGroup, Input, Modal, VerticalGroup, Tag } from '@grafana/ui';
import { EmptySearchResult, HorizontalGroup, Input, Modal, VerticalGroup, Tag } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
@ -63,7 +63,7 @@ const CreateAlertReceiveChannelContainer = observer((props: CreateAlertReceiveCh
</div>
<div className={cx('cards', { cards_centered: !options.length })}>
{options.length ? (
options.map((alertReceiveChannelChoice, index) => {
options.map((alertReceiveChannelChoice) => {
return (
<Block
bordered
@ -95,13 +95,6 @@ const CreateAlertReceiveChannelContainer = observer((props: CreateAlertReceiveCh
<EmptySearchResult>Could not find anything matching your query</EmptySearchResult>
)}
</div>
{/* Need to add documentation link to Integrations
<div className={cx('footer')}>
<Text type="secondary"> Find out how integrations work: </Text>
<a target="_blank" href="https://grafana.com/docs/">
Read documentation
</a>
</div> */}
</Modal>
);
});

View file

@ -22,7 +22,7 @@ export function getSlackMessage(slackError: SlackError, team: Team) {
if (slackError === SlackError.USER_ALREADY_CONNECTED) {
return (
<>Couldnt connect to Slack. This Slack account has already been connected to another user in this organization</>
<>Couldn't connect to Slack. This Slack account has already been connected to another user in this organization</>
);
}

View file

@ -1,5 +1,3 @@
import React from 'react';
import { User } from 'models/user/user.types';
export const getIfChatOpsConnected = (user: User) => {

View file

@ -49,22 +49,6 @@ const EscalationChainCard = observer((props: AlertReceiveChannelCardProps) => {
</Tooltip>
)}
</HorizontalGroup>
{/*<HorizontalGroup>
<PluginLink
query={{ page: 'incidents', integration: alertReceiveChannel.id }}
className={cx('alertsInfoText')}
>
<b>{alertReceiveChannel.alert_count}</b> alerts in <b>{alertReceiveChannel.alert_groups_count}</b>{' '}
incidents
</PluginLink>
<Text type="secondary" size="small">
|
</Text>
<IntegrationLogo scale={0.08} integration={integration} />
<Text type="secondary" size="small">
{integration?.display_name}
</Text>
</HorizontalGroup>*/}
</VerticalGroup>
</HorizontalGroup>
</div>

View file

@ -3,7 +3,6 @@ import React, { ChangeEvent, FC, useCallback, useState } from 'react';
import { Button, Field, HorizontalGroup, Input, Modal } from '@grafana/ui';
import cn from 'classnames/bind';
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
import { useStore } from 'state/useStore';

View file

@ -62,13 +62,10 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => {
{addonBefore}
{escalationPolicyIds ? (
escalationPolicyIds.map((escalationPolicyId, index) => {
// const COLOR_RED = '#FF0000';
const COLOR_RED = '#E60000';
// const STEP_COLORS = ['#52C41A', '#A0D911', '#FADB14', '#FAAD14', COLOR_RED];
const STEP_COLORS = ['#1A7F4B', '#33cc33', '#ffbf00', '#FF8000', COLOR_RED];
const { escalationPolicyStore } = store;
const escalationPolicy = escalationPolicyStore.items[escalationPolicyId];
if (!escalationPolicy) {
@ -102,7 +99,6 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => {
menuShouldPortal
placeholder="Add escalation step..."
onChange={handleCreateEscalationStep}
/* isOptionDisabled={(...rest) => console.log(rest)}*/
options={escalationPolicyStore.webEscalationChoices.map((choice: EscalationPolicyOption) => ({
value: choice.value,
label: choice.create_display_name,

View file

@ -1,12 +1,10 @@
import React, { ReactElement, useCallback, useEffect, useState, useMemo } from 'react';
import React, { ReactElement, useCallback, useEffect } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select, MultiSelect, AsyncMultiSelect, AsyncSelect, Tooltip } from '@grafana/ui';
import { AsyncMultiSelect, AsyncSelect } from '@grafana/ui';
import cn from 'classnames/bind';
import { get, isNil } from 'lodash-es';
import { observer } from 'mobx-react';
import Emoji from 'react-emoji-render';
import { debounce } from 'throttle-debounce';
import { useStore } from 'state/useStore';

View file

@ -40,8 +40,10 @@ const GrafanaTeamSelect = observer((props: GrafanaTeamSelectProps) => {
window.location.search = queryParams.toString();
function mapCurrentPage() {
if (currentPage === 'incident') {return 'incidents'}
return currentPage
if (currentPage === 'incident') {
return 'incidents';
}
return currentPage;
}
};

View file

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { Button, HorizontalGroup, Modal, Select } from '@grafana/ui';
import { Button, HorizontalGroup, Select } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import Emoji from 'react-emoji-render';

View file

@ -1,6 +1,3 @@
import { capitalCase } from 'change-case';
import qs from 'query-string';
import { convertRelativeToAbsoluteDate } from 'utils/datetime';
import { FilterOption } from './IncidentFilters.types';

View file

@ -1,5 +1,3 @@
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { IncidentStatus } from 'models/alertgroup/alertgroup.types';
import { SelectOption } from 'state/types';
export interface IncidentsFiltersType {}

View file

@ -106,7 +106,7 @@ class IncidentsFilters extends Component<IncidentsFiltersProps, IncidentsFilters
return (
<div className={cx('filters')}>
{filters.map((filterOption: FilterOption) => (
<div className={cx('filter')}>
<div key={filterOption.name} className={cx('filter')}>
<Text type="secondary">{capitalCase(filterOption.name)}:</Text> {this.renderFilterOption(filterOption)}
<IconButton size="sm" name="times" onClick={this.getDeleteFilterClickHandler(filterOption.name)} />
</div>
@ -130,24 +130,14 @@ class IncidentsFilters extends Component<IncidentsFiltersProps, IncidentsFilters
renderCards() {
const { store } = this.props;
const {
teamStore: { currentTeam },
} = store;
const { values } = this.state;
const { newIncidents, acknowledgedIncidents, resolvedIncidents, silencedIncidents } = store.alertGroupStore;
const { count: newIncidentsCount, alert_group_rate_to_previous_same_period: newIncidentsRate } = newIncidents;
const { count: acknowledgedIncidentsCount, alert_group_rate_to_previous_same_period: acknowledgedIncidentsRate } =
acknowledgedIncidents;
const { count: resolvedIncidentsCount, alert_group_rate_to_previous_same_period: resolvedIncidentsRate } =
resolvedIncidents;
const { count: silencedIncidentsCount, alert_group_rate_to_previous_same_period: silencedIncidentsRate } =
silencedIncidents;
const { count: newIncidentsCount } = newIncidents;
const { count: acknowledgedIncidentsCount } = acknowledgedIncidents;
const { count: resolvedIncidentsCount } = resolvedIncidents;
const { count: silencedIncidentsCount } = silencedIncidents;
const status = values.status || [];
@ -194,7 +184,7 @@ class IncidentsFilters extends Component<IncidentsFiltersProps, IncidentsFilters
}
handleSearch = (query: string) => {
const { filters, values } = this.state;
const { filters } = this.state;
const searchFilter = filters.find((filter: FilterOption) => filter.name === 'search');
@ -217,7 +207,7 @@ class IncidentsFilters extends Component<IncidentsFiltersProps, IncidentsFilters
};
getDeleteFilterClickHandler = (filterName: FilterOption['name']) => {
const { filters, values } = this.state;
const { filters } = this.state;
return () => {
const newFilters = filters.filter((filterOption: FilterOption) => filterOption.name !== filterName);
@ -306,11 +296,6 @@ class IncidentsFilters extends Component<IncidentsFiltersProps, IncidentsFilters
const value = {
from: dates ? moment(dates[0] + 'Z') : undefined,
to: dates ? moment(dates[1] + 'Z') : undefined,
/* raw: {
from: dates ? moment(dates[0]).format('MMM DD, YYYY hh:mm A') : undefined,
to: dates ? moment(dates[1]).format('MMM DD, YYYY hh:mm A') : undefined,
},*/
raw: {
from: dates ? dates[0] : '',
to: dates ? dates[1] : '',
@ -378,7 +363,7 @@ class IncidentsFilters extends Component<IncidentsFiltersProps, IncidentsFilters
};
getRemoteOptionsChangeHandler = (name: FilterOption['name']) => {
return (value: SelectableValue[], items: any[]) => {
return (value: SelectableValue[], _items: any[]) => {
this.onFiltersValueChange(name, value);
};
};

View file

@ -1,17 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { getLocationSrv, setLocationSrv } from '@grafana/runtime';
import {
Drawer,
Tab,
TabContent,
TabsBar,
IconButton,
Button,
HorizontalGroup,
VerticalGroup,
Input,
} from '@grafana/ui';
import { getLocationSrv } from '@grafana/runtime';
import { Drawer, Tab, TabContent, TabsBar, Button, VerticalGroup, Input } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import CopyToClipboard from 'react-copy-to-clipboard';
@ -21,16 +11,13 @@ import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
import Text from 'components/Text/Text';
import AlertTemplatesFormContainer from 'containers/AlertTemplatesFormContainer/AlertTemplatesFormContainer';
import HeartbeatForm from 'containers/HeartbeatModal/HeartbeatForm';
import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
import { Alert } from 'models/alertgroup/alertgroup.types';
import { SelectOption } from 'state/types';
import { useStore } from 'state/useStore';
import { openNotification } from 'utils';
import { IntegrationSettingsTab } from './IntegrationSettings.types';
import Autoresolve from './parts/Autoresolve';
import LiveLogs from './parts/LiveLogs';
import styles from 'containers/IntegrationSettings/IntegrationSettings.module.css';
@ -74,7 +61,7 @@ const IntegrationSettings = observer((props: IntegrationSettingsProps) => {
const integration = alertReceiveChannelStore.getIntegration(alertReceiveChannel);
const [expanded, setExpanded] = useState(false);
const [expanded, _setExpanded] = useState(false);
const handleSwitchToTemplate = (templateName: string) => {
setSelectedTemplate(templateName);
@ -85,34 +72,17 @@ const IntegrationSettings = observer((props: IntegrationSettingsProps) => {
scrollableContent
expandable
title={
<>
{/*<div className={cx('settings-header-buttons')}>
<IconButton // OMG it blocks 'how to' instructions text selection to copy
name={expanded ? 'angle-right' : 'angle-left'}
size="xl"
surface="header"
onClick={() => {
if (expanded) {
setExpanded(false);
} else {
setExpanded(true);
}
}}
/>
<IconButton name="times" size="xl" surface="header" onClick={onHide} />
</div>*/}
<div className={cx('title')}>
{integration && <IntegrationLogo integration={integration} scale={0.2} />}
<div className={cx('title-column')}>
{alertReceiveChannel && (
<Text.Title level={4}>
<Emoji text={alertReceiveChannel.verbal_name} /> settings
</Text.Title>
)}
{integration && <Text type="secondary">Type: {integration.display_name}</Text>}
</div>
<div className={cx('title')}>
{integration && <IntegrationLogo integration={integration} scale={0.2} />}
<div className={cx('title-column')}>
{alertReceiveChannel && (
<Text.Title level={4}>
<Emoji text={alertReceiveChannel.verbal_name} /> settings
</Text.Title>
)}
{integration && <Text type="secondary">Type: {integration.display_name}</Text>}
</div>
</>
</div>
}
width={expanded ? '100%' : '70%'}
onClose={onHide}
@ -144,14 +114,6 @@ const IntegrationSettings = observer((props: IntegrationSettingsProps) => {
key={IntegrationSettingsTab.Autoresolve}
onChangeTab={getTabClickHandler(IntegrationSettingsTab.Autoresolve)}
/>
{/* Removed untill backend is ready
<Tab
active={activeTab === IntegrationSettingsTab.LiveLogs}
label="Live Logs"
key={IntegrationSettingsTab.LiveLogs}
onChangeTab={getTabClickHandler(IntegrationSettingsTab.LiveLogs)}
/> */}
</TabsBar>
<TabContent className={cx('content')}>
{activeTab === IntegrationSettingsTab.Templates && (
@ -176,7 +138,6 @@ const IntegrationSettings = observer((props: IntegrationSettingsProps) => {
alertGroupId={alertGroupId}
/>
)}
{/*{activeTab === IntegrationSettingsTab.LiveLogs && <LiveLogs alertReceiveChannelId={id} />}*/}
{activeTab === IntegrationSettingsTab.HowToConnect && (
<div className="container">
<VerticalGroup>

View file

@ -6,7 +6,6 @@ import cn from 'classnames/bind';
import { get } from 'lodash-es';
import Block from 'components/GBlock/Block';
import PluginLink from 'components/PluginLink/PluginLink';
import Text from 'components/Text/Text';
import GSelect from 'containers/GSelect/GSelect';
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
@ -55,8 +54,7 @@ const Autoresolve = ({ alertReceiveChannelId, onSwitchToTemplate, alertGroupId }
store.alertReceiveChannelStore.templates[alertReceiveChannelId],
'resolve_condition_template'
);
// @ts-ignore
if (autoresolveCondition == ['invalid template']) {
if (autoresolveCondition === 'invalid template') {
setAutoresolveConditionInvalid(true);
}
}, [store.alertReceiveChannelStore.templates[alertReceiveChannelId]]);

View file

@ -1,6 +1,6 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Alert, Button, EmptySearchResult, LoadingPlaceholder } from '@grafana/ui';
import { Button, EmptySearchResult, LoadingPlaceholder } from '@grafana/ui';
import SourceCode from 'components/SourceCode/SourceCode';
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';

View file

@ -1,4 +1,4 @@
import React, { ReactElement } from 'react';
import React from 'react';
import { SelectableValue } from '@grafana/data';
import Emoji from 'react-emoji-render';

View file

@ -1,6 +0,0 @@
import { Maintenance } from 'models/maintenance/maintenance.types';
import { Schedule } from 'models/schedule/schedule.types';
export function prepareForEdit(item: Maintenance) {
return {};
}

View file

@ -1,15 +1,12 @@
import React, { HTMLAttributes, useCallback, useEffect, useRef, useState } from 'react';
import React, { HTMLAttributes, useEffect, useState } from 'react';
import { Button, HorizontalGroup, Icon, LoadingPlaceholder } from '@grafana/ui';
import { Button, LoadingPlaceholder } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import GTable from 'components/GTable/GTable';
import Text from 'components/Text/Text';
import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types';
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
import { User } from 'models/user/user.types';
import { makeRequest } from 'network';
import { useStore } from 'state/useStore';
import { UserAction } from 'state/userAction';
@ -33,8 +30,6 @@ const MobileAppVerification = observer((props: MobileAppVerificationProps) => {
const isCurrent = userStore.currentUserPk === user.pk;
const action = isCurrent ? UserAction.UpdateOwnSettings : UserAction.UpdateOtherUsersSettings;
const { id = UserSettingsTab.UserInfo } = props;
const [showMobileAppVerificationToken, setShowMobileAppVerificationToken] = useState<string>(undefined);
const [isMobileAppVerificationTokenExisting, setIsMobileAppVerificationTokenExisting] = useState<boolean>(false);
const [MobileAppVerificationTokenLoading, setMobileAppVerificationTokenLoading] = useState<boolean>(true);
@ -42,11 +37,11 @@ const MobileAppVerification = observer((props: MobileAppVerificationProps) => {
useEffect(() => {
userStore
.getMobileAppVerificationToken(userPk)
.then((res) => {
.then((_res) => {
setIsMobileAppVerificationTokenExisting(true);
setMobileAppVerificationTokenLoading(false);
})
.catch((res) => {
.catch((_res) => {
setIsMobileAppVerificationTokenExisting(false);
setMobileAppVerificationTokenLoading(false);
});
@ -59,27 +54,6 @@ const MobileAppVerification = observer((props: MobileAppVerificationProps) => {
.then((res) => setShowMobileAppVerificationToken(res?.token));
};
// const [devices, setDevices] = useState();
//
// const updateDevices = useCallback(() => {
// makeRequest(`/device/apns/`, {
// method: 'GET',
// }).then((data) => {
// setDevices(data);
// });
// }, []);
//
// useEffect(() => {
// updateDevices();
// }, []);
//
// const columns = [
// {
// title: 'Name',
// dataIndex: 'name',
// },
// ];
return (
<div className={cx('mobile-app-settings')}>
{MobileAppVerificationTokenLoading ? (
@ -114,41 +88,23 @@ const MobileAppVerification = observer((props: MobileAppVerificationProps) => {
)}
</>
) : (
<>
<p>
<WithPermissionControl userAction={action}>
<Button
onClick={handleCreateMobileAppVerificationToken}
className={cx('iCal-button')}
variant="secondary"
>
Get the code
</Button>
</WithPermissionControl>
</p>
</>
<p>
<WithPermissionControl userAction={action}>
<Button
onClick={handleCreateMobileAppVerificationToken}
className={cx('iCal-button')}
variant="secondary"
>
Get the code
</Button>
</WithPermissionControl>
</p>
)}
<p>
<Text>* Only iOS is currently supported</Text>
</p>
</>
)}
{/*<>*/}
{/* <GTable*/}
{/* title={() => (*/}
{/* <div className={cx('header')}>*/}
{/* <HorizontalGroup align="flex-end">*/}
{/* <Text.Title level={4}>Your devices</Text.Title>*/}
{/* </HorizontalGroup>*/}
{/* </div>*/}
{/* )}*/}
{/* rowKey="id"*/}
{/* className="api-keys"*/}
{/* data={devices}*/}
{/* emptyText={devices ? 'No devices connected' : 'Loading...'}*/}
{/* columns={columns}*/}
{/* />*/}
{/*</>*/}
</div>
);
});

View file

@ -1,12 +1,11 @@
import React, { ChangeEvent, useCallback, useMemo, useState } from 'react';
import { RawTimeRange } from '@grafana/data';
import { Button, HorizontalGroup, Input, TimeRangeInput } from '@grafana/ui';
import { HorizontalGroup, Input, TimeRangeInput } from '@grafana/ui';
import cn from 'classnames/bind';
import { observer } from 'mobx-react';
import RemoteSelect from 'containers/RemoteSelect/RemoteSelect';
import { useStore } from 'state/useStore';
import styles from './OrganizationLogFilters.module.css';
@ -23,8 +22,6 @@ const OrganizationLogFilters = observer((props: OrganizationLogFiltersProps) =>
const [createAtRaw, setCreateAtRaw] = useState<RawTimeRange>();
const store = useStore();
const onSearchTermChangeCallback = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const filters = {
@ -46,10 +43,6 @@ const OrganizationLogFilters = observer((props: OrganizationLogFiltersProps) =>
};
};
const handleClear = useCallback(() => {
onChange({});
}, [onChange]);
const handleChangeCreatedAt = useCallback(
(filter) => {
onChange({

View file

@ -1,7 +1,4 @@
import { ReactElement } from 'react';
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
import { DEFAULT_USER_ROLES } from 'models/user/user.config';
export const form: { name: string; fields: FormItem[] } = {
name: 'OutgoingWebhook',

View file

@ -2,16 +2,8 @@ import React, { useCallback, useEffect, useState } from 'react';
import { AppPluginMeta, PluginConfigPageProps } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import {
Button,
Field,
HorizontalGroup,
VerticalGroup,
Input,
Label,
Legend,
LoadingPlaceholder,
} from '@grafana/ui';
import { Button, Field, HorizontalGroup, VerticalGroup, Input, Label, Legend, LoadingPlaceholder } from '@grafana/ui';
import { AxiosError } from 'axios';
import cn from 'classnames/bind';
import { OnCallAppSettings } from 'types';
@ -23,9 +15,10 @@ import { makeRequest } from 'network';
import {
createGrafanaToken,
getPluginSyncStatus,
startPluginSync, SYNC_STATUS_RETRY_LIMIT,
startPluginSync,
SYNC_STATUS_RETRY_LIMIT,
syncStatusDelay,
updateGrafanaToken
updateGrafanaToken,
} from 'state/plugin';
import { GRAFANA_LICENSE_OSS } from 'utils/consts';
import { getItem, setItem } from 'utils/localStorage';
@ -49,7 +42,9 @@ export const PluginConfigPage = (props: Props) => {
const [isSelfHostedInstall, setIsSelfHostedInstall] = useState<boolean>(true);
const [retrySync, setRetrySync] = useState<boolean>(false);
const INVALID_INVITE_TOKEN_ERROR_MSG = `It seems like your invite token may be invalid. ${constructErrorActionMessage('generating a new invite token')}`;
const INVALID_INVITE_TOKEN_ERROR_MSG = `It seems like your invite token may be invalid. ${constructErrorActionMessage(
'generating a new invite token'
)}`;
const setupPlugin = useCallback(async () => {
setItem('onCallApiUrl', onCallApiUrl);
@ -134,23 +129,30 @@ export const PluginConfigPage = (props: Props) => {
setGrafanaUrl(e.target.value);
}, []);
const handleSyncException = useCallback((e) => {
const buildErrMsg = (msg: string): string =>
constructSyncErrorMessage(msg, plugin.meta.jsonData?.onCallApiUrl);
const handleSyncException = useCallback((e: AxiosError) => {
const buildErrMsg = (msg: string): string => constructSyncErrorMessage(msg, plugin.meta.jsonData?.onCallApiUrl);
if (plugin.meta.jsonData?.onCallApiUrl) {
const { status: statusCode } = e.response;
const { status: statusCode } = e.response;
let statusMessage: string;
if (statusCode == 403) {
if (statusCode === 403) {
statusMessage = buildErrMsg(INVALID_INVITE_TOKEN_ERROR_MSG);
} else if (statusCode === 404) {
statusMessage = buildErrMsg('If Grafana OnCall was just installed, restart Grafana for OnCall routes to be available.');
statusMessage = buildErrMsg(
'If Grafana OnCall was just installed, restart Grafana for OnCall routes to be available.'
);
} else if (statusCode === 502) {
statusMessage = buildErrMsg(`Unable to communicate with either the Grafana API, or Grafana OnCall engine API. ${constructErrorActionMessage('verify that the API URLs that you entered are correct')}`);
statusMessage = buildErrMsg(
`Unable to communicate with either the Grafana API, or Grafana OnCall engine API. ${constructErrorActionMessage(
'verify that the API URLs that you entered are correct'
)}`
);
} else {
statusMessage = buildErrMsg(`An unknown error occured. ${constructErrorActionMessage()}. If the error still occurs please reach out to support.`)
statusMessage = buildErrMsg(
`An unknown error occured. ${constructErrorActionMessage()}. If the error still occurs please reach out to support.`
);
}
setPluginStatusMessage(statusMessage);
setRetrySync(true);
@ -168,17 +170,18 @@ export const PluginConfigPage = (props: Props) => {
? ` (${getSyncResponse.license}, ${getSyncResponse.version})`
: '';
let pluginStatusMessage = `Connected to OnCall${versionInfo}\n - OnCall URL: ${plugin.meta.jsonData.onCallApiUrl}\n`
let pluginStatusMessage = `Connected to OnCall${versionInfo}\n - OnCall URL: ${plugin.meta.jsonData.onCallApiUrl}\n`;
if (plugin.meta.jsonData.grafanaUrl) {
pluginStatusMessage = `${pluginStatusMessage} - Grafana URL: ${plugin.meta.jsonData.grafanaUrl}`
pluginStatusMessage = `${pluginStatusMessage} - Grafana URL: ${plugin.meta.jsonData.grafanaUrl}`;
}
setPluginStatusMessage(pluginStatusMessage)
setPluginStatusMessage(pluginStatusMessage);
setIsSelfHostedInstall(plugin.meta.jsonData?.license === GRAFANA_LICENSE_OSS);
setPluginStatusOk(true);
} else {
setPluginStatusMessage(constructSyncErrorMessage(INVALID_INVITE_TOKEN_ERROR_MSG,
plugin.meta.jsonData.grafanaUrl));
setPluginStatusMessage(
constructSyncErrorMessage(INVALID_INVITE_TOKEN_ERROR_MSG, plugin.meta.jsonData.grafanaUrl)
);
setRetrySync(true);
}
setPluginConfigLoading(false);
@ -195,16 +198,16 @@ export const PluginConfigPage = (props: Props) => {
return;
}
getPluginSyncStatus().then((get_sync_response) => {
if (get_sync_response.hasOwnProperty('token_ok')) {
finishSync(get_sync_response);
} else {
syncStatusDelay(retryCount + 1).then(() => waitForSyncStatus(retryCount + 1))
}
}).catch((e) => {
handleSyncException(e);
});
}
getPluginSyncStatus()
.then((get_sync_response) => {
if (get_sync_response.hasOwnProperty('token_ok')) {
finishSync(get_sync_response);
} else {
syncStatusDelay(retryCount + 1).then(() => waitForSyncStatus(retryCount + 1));
}
})
.catch(handleSyncException);
};
const startSync = useCallback(() => {
setRetrySync(false);
@ -214,9 +217,7 @@ export const PluginConfigPage = (props: Props) => {
.catch(handleSyncException);
}, []);
useEffect(() => {
startSync();
}, []);
useEffect(startSync, []);
return (
<div>

View file

@ -1,5 +1,6 @@
export const constructSyncErrorMessage = (errMsg: string, url?: string): string =>
`${url ? `${url}\n` : ''}${errMsg}`;
export const constructSyncErrorMessage = (errMsg: string, url?: string): string => `${url ? `${url}\n` : ''}${errMsg}`;
export const constructErrorActionMessage = (msg?: string): string =>
`Try removing your current configuration, ${msg ? msg : 'double checking your settings'}, and re-initializing the plugin.\nBy removing your current configuration, you will need to ensure that you regenerate a new invite token, and input this in your new configuration.`
`Try removing your current configuration, ${
msg ? msg : 'double checking your settings'
}, and re-initializing the plugin.\nBy removing your current configuration, you will need to ensure that you regenerate a new invite token, and input this in your new configuration.`;

View file

@ -1,3 +0,0 @@
.root {
display: block;
}

View file

@ -2,15 +2,10 @@ import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
import { SelectableValue } from '@grafana/data';
import { AsyncMultiSelect, AsyncSelect } from '@grafana/ui';
import cn from 'classnames/bind';
import { inject, observer } from 'mobx-react';
import { makeRequest } from 'network';
import styles from './RemoteSelect.module.css';
const cx = cn.bind(styles);
interface RemoteSelectProps {
autoFocus?: boolean;
href: string;

View file

@ -1,14 +1,12 @@
import React, { FC, useMemo, useState, useEffect, useRef, useCallback } from 'react';
import React, { FC, useMemo, useState } from 'react';
import { HorizontalGroup, LoadingPlaceholder } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import ScheduleSlot from 'containers/ScheduleSlot/ScheduleSlot';
import { getFromString } from 'models/schedule/schedule.helpers';
import { Rotation as RotationType, Schedule, Event } from 'models/schedule/schedule.types';
import { Schedule, Event } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { usePrevious } from 'utils/hooks';
import { getLabel } from './Rotation.helpers';
@ -16,8 +14,6 @@ import styles from './Rotation.module.css';
const cx = cn.bind(styles);
interface ScheduleSlotState {}
interface RotationProps {
scheduleId: Schedule['id'];
startMoment: dayjs.Dayjs;
@ -45,38 +41,7 @@ const Rotation: FC<RotationProps> = (props) => {
transparent = false,
} = props;
const [animate, setAnimate] = useState<boolean>(true);
const [width, setWidth] = useState<number | undefined>();
const startMomentString = useMemo(() => getFromString(startMoment), [startMoment]);
const prevStartMomentString = usePrevious(startMomentString);
// console.log(events);
// const rotation = store.scheduleStore.rotations[id]?.[prevStartMomentString];
/* useEffect(() => {
setTransparent(false);
}, [rotation]);
useEffect(() => {
setTransparent(true);
}, [startMoment]);*/
useEffect(() => {
const startMomentString = startMoment.utc().format('YYYY-MM-DDTHH:mm:ss.000Z');
// console.log('CHANGE START MOMENT', startMomentString);
// store.scheduleStore.updateEvents(scheduleId, startMomentString, currentTimezone);
}, [startMomentString]);
const slots = useCallback((node) => {
if (node) {
setWidth(node.offsetWidth);
}
}, []);
const [animate, _setAnimate] = useState<boolean>(true);
const handleClick = (event) => {
const rect = event.currentTarget.getBoundingClientRect();
@ -94,11 +59,8 @@ const Rotation: FC<RotationProps> = (props) => {
}
const firstShift = events[0];
const firstShiftOffset = dayjs(firstShift.start).diff(startMoment, 'seconds');
const base = 60 * 60 * 24 * days;
// const utcOffset = dayjs().tz(currentTimezone).utcOffset();
return firstShiftOffset / base;
}, [events]);

View file

@ -1,20 +1,10 @@
import React, { useCallback, useMemo } from 'react';
import { DateTime, dateTime } from '@grafana/data';
import { DatePickerWithInput, HorizontalGroup, TimeOfDayPicker, Tooltip } from '@grafana/ui';
import cn from 'classnames/bind';
import { DatePickerWithInput, HorizontalGroup, TimeOfDayPicker } from '@grafana/ui';
import dayjs from 'dayjs';
import { observer } from 'mobx-react';
import { Moment } from 'moment-timezone';
import { Timezone } from 'models/timezone/timezone.types';
import { getUserNotificationsSummary } from 'models/user/user.helpers';
import { User } from 'models/user/user.types';
import { useStore } from 'state/useStore';
import styles from 'containers/UserTooltip/UserTooltip.module.css';
const cx = cn.bind(styles);
interface UserTooltipProps {
value: dayjs.Dayjs;

View file

@ -1,18 +1,6 @@
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { dateTime, DateTime } from '@grafana/data';
import {
IconButton,
VerticalGroup,
HorizontalGroup,
Field,
Input,
Button,
Select,
InlineSwitch,
DatePickerWithInput,
TimeOfDayPicker,
} from '@grafana/ui';
import { IconButton, VerticalGroup, HorizontalGroup, Field, Button, Select, InlineSwitch } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { observer } from 'mobx-react';
@ -21,16 +9,14 @@ import Draggable from 'react-draggable';
import Modal from 'components/Modal/Modal';
import Text from 'components/Text/Text';
import UserGroups from 'components/UserGroups/UserGroups';
import { Item } from 'components/UserGroups/UserGroups.types';
import WithConfirm from 'components/WithConfirm/WithConfirm';
import WorkingHours from 'components/WorkingHours/WorkingHours';
import RemoteSelect from 'containers/RemoteSelect/RemoteSelect';
import { getFromString } from 'models/schedule/schedule.helpers';
import { Rotation, Schedule, Shift } from 'models/schedule/schedule.types';
import { Schedule, Shift } from 'models/schedule/schedule.types';
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
import { makeRequest } from 'network';
import { getDateTime, getUTCString } from 'pages/schedule/Schedule.helpers';
import { SelectOption } from 'state/types';
import { useStore } from 'state/useStore';
@ -38,7 +24,6 @@ import { getCoords, waitForElement } from 'utils/DOM';
import { useDebouncedCallback } from 'utils/hooks';
import DateTimePicker from './DateTimePicker';
import { RotationCreateData } from './RotationForm.types';
import styles from './RotationForm.module.css';
@ -77,10 +62,8 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
shiftColor = '#3D71D9',
} = props;
// console.log('shiftColor', shiftColor);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [offsetTop, setOffsetTop] = useState<number>(0);
const [repeatEveryValue, setRepeatEveryValue] = useState<number>(1);
const [repeatEveryPeriod, setRepeatEveryPeriod] = useState<number>(0);
const [selectedDays, setSelectedDays] = useState<string[]>([]);
@ -90,6 +73,9 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
const [endLess, setEndless] = useState<boolean>(true);
const [rotationEnd, setRotationEnd] = useState<dayjs.Dayjs>(shiftMoment.add(1, 'month'));
const store = useStore();
const shift = store.scheduleStore.shifts[shiftId];
useEffect(() => {
if (rotationStart.isBefore(shiftStart)) {
setRotationStart(shiftStart);
@ -106,22 +92,12 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
[shiftStart, shiftEnd]
);
const store = useStore();
const shift = store.scheduleStore.shifts[shiftId];
const [offsetTop, setOffsetTop] = useState<number>(0);
useEffect(() => {
if (isOpen) {
waitForElement(`#layer${shiftId === 'new' ? layerPriority : shift?.priority_level}`).then((elm) => {
const modal = document.querySelector(`.${cx('draggable')}`) as HTMLDivElement;
const coords = getCoords(elm);
// elm.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
// setOffsetTop(Math.max(coords.top + elm.offsetHeight, 0));
const offsetTop = Math.min(
Math.max(coords.top - modal?.offsetHeight - 10, 10),
document.body.offsetHeight - modal?.offsetHeight - 10
@ -277,8 +253,6 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
</HorizontalGroup>
</Text>
<HorizontalGroup>
{/*<IconButton disabled variant="secondary" tooltip="Copy" name="copy" />
<IconButton disabled variant="secondary" tooltip="Code" name="brackets-curly" />*/}
{shiftId !== 'new' && (
<WithConfirm>
<IconButton variant="secondary" tooltip="Delete" name="trash-alt" onClick={handleDeleteClick} />
@ -287,7 +261,6 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
<IconButton variant="secondary" className={cx('drag-handler')} name="draggabledots" />
</HorizontalGroup>
</HorizontalGroup>
{/*<hr />*/}
<div className={cx('content')}>
<VerticalGroup>
<div className={cx('two-fields')}>
@ -348,7 +321,6 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
</Field>
</HorizontalGroup>
{repeatEveryPeriod === 1 && (
/*<HorizontalGroup justify="center">*/
<Field label="Select days to repeat">
<DaysSelector
options={store.scheduleStore.byDayOptions}
@ -356,7 +328,6 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
onChange={(value) => setSelectedDays(value)}
/>
</Field>
/*</HorizontalGroup>*/
)}
<div className={cx('two-fields')}>
<Field
@ -429,6 +400,7 @@ const DaysSelector = ({ value, onChange, options }: DaysSelectorProps) => {
<div className={cx('days')}>
{options.map(({ display_name, value: itemValue }) => (
<div
key={display_name}
onClick={getDayClickHandler(itemValue as string)}
className={cx('day', { day__selected: value.includes(itemValue as string) })}
>

View file

@ -1,7 +1,6 @@
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { dateTime, DateTime } from '@grafana/data';
import { IconButton, VerticalGroup, HorizontalGroup, Field, Input, Button, Select, InlineSwitch } from '@grafana/ui';
import { IconButton, VerticalGroup, HorizontalGroup, Field, Button } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import Draggable from 'react-draggable';
@ -12,7 +11,7 @@ import UserGroups from 'components/UserGroups/UserGroups';
import WithConfirm from 'components/WithConfirm/WithConfirm';
import WorkingHours from 'components/WorkingHours/WorkingHours';
import { getFromString } from 'models/schedule/schedule.helpers';
import { Rotation, Schedule, Shift } from 'models/schedule/schedule.types';
import { Schedule, Shift } from 'models/schedule/schedule.types';
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
@ -22,7 +21,6 @@ import { getCoords, waitForElement } from 'utils/DOM';
import { useDebouncedCallback } from 'utils/hooks';
import DateTimePicker from './DateTimePicker';
import { RotationCreateData } from './RotationForm.types';
import styles from './RotationForm.module.css';
@ -189,8 +187,6 @@ const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {
<HorizontalGroup justify="space-between">
<Text size="medium">{shiftId === 'new' ? 'New Override' : 'Update Override'}</Text>
<HorizontalGroup>
{/*<IconButton disabled variant="secondary" tooltip="Copy" name="copy" />
<IconButton disabled variant="secondary" tooltip="Code" name="brackets-curly" />*/}
{shiftId !== 'new' && (
<WithConfirm>
<IconButton variant="secondary" tooltip="Delete" name="trash-alt" onClick={handleDeleteClick} />

View file

@ -7,8 +7,8 @@ export const findColor = (shiftId: Shift['id'], layers: Layer[], overrides?) =>
let layerIndex = -1;
let rotationIndex = -1;
if (layers) {
outer: for (var i = 0; i < layers.length; i++) {
for (var j = 0; j < layers[i].shifts.length; j++) {
outer: for (let i = 0; i < layers.length; i++) {
for (let j = 0; j < layers[i].shifts.length; j++) {
const shift = layers[i].shifts[j];
if (shift.shiftId === shiftId || (shiftId === 'new' && shift.isPreview)) {
layerIndex = i;
@ -21,7 +21,7 @@ export const findColor = (shiftId: Shift['id'], layers: Layer[], overrides?) =>
let overrideIndex = -1;
if (layerIndex === -1 && rotationIndex === -1 && overrides) {
for (var k = 0; k < overrides.length; k++) {
for (let k = 0; k < overrides.length; k++) {
const shift = overrides[k];
if (shift.shiftId === shiftId || (shiftId === 'new' && shift.isPreview)) {
overrideIndex = k;

View file

@ -1,11 +1,9 @@
import React, { Component, useMemo, useState } from 'react';
import React, { Component } from 'react';
import { SelectableValue } from '@grafana/data';
import { ValuePicker, IconButton, Icon, HorizontalGroup, Button, LoadingPlaceholder } from '@grafana/ui';
import { ValuePicker, HorizontalGroup, Button } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { toJS } from 'mobx';
import { observer } from 'mobx-react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
@ -13,11 +11,10 @@ import Text from 'components/Text/Text';
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
import Rotation from 'containers/Rotation/Rotation';
import RotationForm from 'containers/RotationForm/RotationForm';
import { RotationCreateData } from 'containers/RotationForm/RotationForm.types';
import { getColor, getFromString } from 'models/schedule/schedule.helpers';
import { Event, Layer, Schedule, Shift } from 'models/schedule/schedule.types';
import { Layer, Schedule, Shift } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { SelectOption, WithStoreProps } from 'state/types';
import { WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
import { DEFAULT_TRANSITION_TIMEOUT } from './Rotations.config';
@ -122,7 +119,6 @@ class Rotations extends Component<RotationsProps, RotationsState> {
<div className={cx('layer-title')}>
<HorizontalGroup spacing="sm" justify="center">
<Text type="secondary">Layer {layer.priority}</Text>
{/*<Icon name="info-circle" />*/}
</HorizontalGroup>
</div>
<div className={cx('rotations')}>

View file

@ -1,6 +1,6 @@
import React, { Component, useEffect } from 'react';
import React, { Component } from 'react';
import { Button, HorizontalGroup, Icon, Input, ValuePicker } from '@grafana/ui';
import { HorizontalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { observer } from 'mobx-react';
@ -9,8 +9,8 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group';
import Text from 'components/Text/Text';
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
import Rotation from 'containers/Rotation/Rotation';
import { getColor, getFromString, getLayersFromStore, getOverrideColor, getOverridesFromStore, getShiftsFromStore } from 'models/schedule/schedule.helpers';
import { Event, Layer, Schedule, Shift } from 'models/schedule/schedule.types';
import { getLayersFromStore, getOverridesFromStore, getShiftsFromStore } from 'models/schedule/schedule.helpers';
import { Schedule, Shift } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
@ -51,7 +51,7 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState
const shifts = getShiftsFromStore(store, scheduleId, startMoment);
const layers = getLayersFromStore(store, scheduleId, startMoment);
const overrides = getOverridesFromStore(store, scheduleId, startMoment);
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
@ -67,12 +67,6 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState
Final schedule
</Text.Title>
</div>
{/*<Input
prefix={<Icon name="search" />}
placeholder="Search..."
value={searchTerm}
onChange={this.onSearchTermChangeCallback}
/>*/}
</HorizontalGroup>
</div>
)}

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { Button, HorizontalGroup, Icon, ValuePicker } from '@grafana/ui';
import { Button, HorizontalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { observer } from 'mobx-react';
@ -9,15 +9,9 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group';
import Text from 'components/Text/Text';
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
import Rotation from 'containers/Rotation/Rotation';
import { RotationCreateData } from 'containers/RotationForm/RotationForm.types';
import ScheduleOverrideForm from 'containers/RotationForm/ScheduleOverrideForm';
import {
getFromString,
getOverrideColor,
getOverridesFromStore,
getShiftsFromStore,
} from 'models/schedule/schedule.helpers';
import { Event, Schedule, Shift, ShiftEvents } from 'models/schedule/schedule.types';
import { getOverrideColor, getOverridesFromStore } from 'models/schedule/schedule.helpers';
import { Schedule, Shift, ShiftEvents } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
@ -125,9 +119,6 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
)}
</TransitionGroup>
</div>
{/* <div className={cx('add-rotations-layer')} onClick={this.handleAddOverride}>
+ Add override
</div>*/}
</div>
{shiftIdToShowRotationForm && (
<ScheduleOverrideForm

View file

@ -1,8 +1,5 @@
import { ReactElement } from 'react';
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config';
import { DEFAULT_USER_ROLES } from 'models/user/user.config';
const commonFields: FormItem[] = [
{
@ -92,12 +89,6 @@ const commonFields: FormItem[] = [
},
description: 'Specify how to notify a team member when their shift is the next one scheduled',
},
// {
// name: 'send_empty_shifts_report',
// normalize: (value) => Boolean(value),
// label: 'Send reports about empty shifts to Slack',
// type: FormItemType.Switch,
// },
];
export const iCalForm: { name: string; fields: FormItem[] } = {

Some files were not shown because too many files have changed in this diff Show more