Main Grouping&Templating PR fro all frontend changes (#1731)
# What this PR does Main Grouping&Templating PR fro all frontend changes: Includes: 1. Integration table view 2. Integration form using Drawer component 3. Integration landing page with routes/escalation chains 4. Templates 5. Groupong ## Which issue(s) this PR fixes https://github.com/grafana/oncall/issues/1620 https://github.com/grafana/oncall/issues/1621 --------- Co-authored-by: Rares Mardare <rares.mardare@grafana.com> Co-authored-by: Ildar Iskhakov <ildar.iskhakov@grafana.com>
This commit is contained in:
parent
d198b932c1
commit
b10b589f72
100 changed files with 4878 additions and 5191 deletions
|
|
@ -4,6 +4,8 @@ from django_filters.rest_framework import DjangoFilterBackend
|
|||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import SearchFilter
|
||||
|
||||
# from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
|
@ -31,6 +33,12 @@ from common.api_helpers.mixins import (
|
|||
from common.exceptions import MaintenanceCouldNotBeStartedError, TeamCanNotBeChangedError, UnableToSendDemoAlert
|
||||
from common.insight_log import EntityEvent, write_resource_insight_log
|
||||
|
||||
# class AlertReceiveChannelPagination(PageNumberPagination):
|
||||
# page_size = 25
|
||||
# page_query_param = "page"
|
||||
# page_size_query_param = "perpage"
|
||||
# max_page_size = 50
|
||||
|
||||
|
||||
class AlertReceiveChannelFilter(ByTeamModelFieldFilterMixin, filters.FilterSet):
|
||||
maintenance_mode = filters.MultipleChoiceFilter(
|
||||
|
|
@ -81,6 +89,7 @@ class AlertReceiveChannelView(
|
|||
search_fields = ("verbal_name",)
|
||||
|
||||
filterset_class = AlertReceiveChannelFilter
|
||||
# pagination_class = AlertReceiveChannelPagination
|
||||
|
||||
rbac_permissions = {
|
||||
"metadata": [RBACPermission.Permissions.INTEGRATIONS_READ],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const esModules = ['react-colorful', 'uuid', 'ol'].join('|');
|
||||
const esModules = ['@grafana', '@grafana/ui', 'ol', 'd3-interpolate', 'd3-color', 'react-colorful', 'uuid'].join('|');
|
||||
|
||||
module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@
|
|||
"@grafana/faro-web-sdk": "^1.0.0-beta4",
|
||||
"@grafana/faro-web-tracing": "^1.0.0-beta4",
|
||||
"@grafana/runtime": "9.3.0-beta1",
|
||||
"@grafana/ui": "^9.2.4",
|
||||
"@grafana/ui": "^9.4.7",
|
||||
"@opentelemetry/api": "^1.3.0",
|
||||
"array-move": "^4.0.0",
|
||||
"change-case": "^4.1.1",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,143 @@ export interface Template {
|
|||
group: string;
|
||||
}
|
||||
|
||||
export interface TemplateForEdit {
|
||||
displayName: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
additionalData?: {
|
||||
chatOpsName?: string;
|
||||
data?: string;
|
||||
additionalDescription?: string;
|
||||
};
|
||||
isRoute?: boolean;
|
||||
}
|
||||
|
||||
export const templateForEdit: { [id: string]: TemplateForEdit } = {
|
||||
web_title_template: {
|
||||
displayName: 'Web title',
|
||||
name: 'web_title_template',
|
||||
description:
|
||||
'Same for: phone call, sms, mobile push, mobile app title, email title, slack title, ms teams title, telegram title.',
|
||||
},
|
||||
web_message_template: {
|
||||
displayName: 'Web message',
|
||||
name: 'web_message_template',
|
||||
description:
|
||||
'Same for: phone call, sms, mobile push, mobile app title, email title, slack title, ms teams title, telegram title.',
|
||||
},
|
||||
slack_title_template: {
|
||||
name: 'slack_title_template',
|
||||
displayName: 'Slack title',
|
||||
description: '',
|
||||
additionalData: {
|
||||
chatOpsName: 'slack',
|
||||
data: 'Click "Acknowledge" and then "Unacknowledge" in Slack to trigger re-rendering.',
|
||||
},
|
||||
},
|
||||
sms_title_template: {
|
||||
name: 'sms_title_template',
|
||||
displayName: 'Sms title',
|
||||
description: '',
|
||||
},
|
||||
phone_call_title_template: {
|
||||
name: 'phone_call_title_template',
|
||||
displayName: 'Phone call title',
|
||||
description: '',
|
||||
},
|
||||
email_title_template: {
|
||||
name: 'email_title_template',
|
||||
displayName: 'Email title',
|
||||
description: '',
|
||||
},
|
||||
telegram_title_template: {
|
||||
name: 'telegram_title_template',
|
||||
displayName: 'Telegram title',
|
||||
description: '',
|
||||
additionalData: {
|
||||
chatOpsName: 'telegram',
|
||||
},
|
||||
},
|
||||
slack_message_template: {
|
||||
name: 'slack_message_template',
|
||||
displayName: 'Slack message',
|
||||
description: '',
|
||||
additionalData: {
|
||||
chatOpsName: 'slack',
|
||||
data: 'Click "Acknowledge" and then "Unacknowledge" in Slack to trigger re-rendering.',
|
||||
},
|
||||
},
|
||||
email_message_template: {
|
||||
name: 'email_message_template',
|
||||
displayName: 'Email message',
|
||||
description: '',
|
||||
},
|
||||
telegram_message_template: {
|
||||
name: 'telegram_message_template',
|
||||
displayName: 'Telegram message',
|
||||
description: '',
|
||||
additionalData: {
|
||||
chatOpsName: 'telegram',
|
||||
},
|
||||
},
|
||||
slack_image_url_template: {
|
||||
name: 'slack_image_url_template',
|
||||
displayName: 'Slack image url',
|
||||
description: '',
|
||||
additionalData: {
|
||||
chatOpsName: 'slack',
|
||||
data: 'Click "Acknowledge" and then "Unacknowledge" in Slack to trigger re-rendering.',
|
||||
},
|
||||
},
|
||||
web_image_url_template: {
|
||||
name: 'web_image_url_template',
|
||||
displayName: 'Web image url',
|
||||
description:
|
||||
'Same for: phone call, sms, mobile push, mobile app title, email title, slack title, ms teams title, telegram title.',
|
||||
},
|
||||
telegram_image_url_template: {
|
||||
name: 'telegram_image_url_template',
|
||||
displayName: 'Telegram image url',
|
||||
description: '',
|
||||
additionalData: {
|
||||
chatOpsName: 'telegram',
|
||||
},
|
||||
},
|
||||
grouping_id_template: {
|
||||
name: 'grouping_id_template',
|
||||
displayName: 'Grouping',
|
||||
description:
|
||||
'Reduce noise, minimize duplication with Alert Grouping, based on time, alert content, and even multiple features at the same time. Check the cheasheet to customize your template.',
|
||||
},
|
||||
acknowledge_condition_template: {
|
||||
name: 'acknowledge_condition_template',
|
||||
displayName: 'Acknowledge condition',
|
||||
description: '',
|
||||
},
|
||||
resolve_condition_template: {
|
||||
name: 'resolve_condition_template',
|
||||
displayName: 'Resolve condition',
|
||||
description:
|
||||
'When monitoring systems return to normal, they can send "resolve" alerts. OnCall can use these signals to resolve alert groups accordingly.',
|
||||
},
|
||||
source_link_template: {
|
||||
name: 'source_link_template',
|
||||
displayName: 'Source link',
|
||||
description: '',
|
||||
},
|
||||
routing: {
|
||||
name: 'routing',
|
||||
displayName: 'Routing',
|
||||
description:
|
||||
'Routes direct alerts to different escalation chains based on the content, such as severity or region.',
|
||||
additionalData: {
|
||||
additionalDescription: 'For an alert to be directed to this route, the template must evaluate to True.',
|
||||
data: 'Selected Alert will be directed to this route',
|
||||
},
|
||||
isRoute: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const templatesToRender: Template[] = [
|
||||
{
|
||||
name: 'web_title_template',
|
||||
|
|
|
|||
154
grafana-plugin/src/components/CheatSheet/CheatSheet.config.ts
Normal file
154
grafana-plugin/src/components/CheatSheet/CheatSheet.config.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
export interface CheatSheetItem {
|
||||
name: string;
|
||||
listItems?: Array<{
|
||||
listItemName?: string;
|
||||
codeExample?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface CheatSheetInterface {
|
||||
name: string;
|
||||
description: string;
|
||||
fields: CheatSheetItem[];
|
||||
}
|
||||
|
||||
export const groupingTemplateCheatSheet: CheatSheetInterface = {
|
||||
name: 'Grouping template cheatsheet',
|
||||
description: 'Jinja2 is used for templating ( docs). ',
|
||||
fields: [
|
||||
{
|
||||
name: 'Additional variables and functions',
|
||||
listItems: [
|
||||
{ listItemName: 'time(), datetimeformat, iso8601_to_time' },
|
||||
{ listItemName: 'to_pretty_json' },
|
||||
{ listItemName: 'regex_replace, regex_match' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Examples',
|
||||
listItems: [
|
||||
{ listItemName: 'group every hour', codeExample: '{{ time() | datetimeformat("%d-%m-%Y %H") }}' },
|
||||
{ listItemName: 'group every X hours', codeExample: '{{ every_hour(5) }}' },
|
||||
{ listItemName: 'group alerts every microsecond (every 0.000001 second)', codeExample: '{{ time() }}' },
|
||||
{ listItemName: 'group based on the specific field', codeExample: '{{ payload.uuid }}' },
|
||||
{ listItemName: 'group based on multiple fields', codeExample: '{{ payload.uuid }} \n {{ payload.region }}' },
|
||||
{
|
||||
listItemName: 'group alerts with the same uuid, create new group every hour',
|
||||
codeExample: '{{ payload.uuid }} \n {{ time() | datetimeformat("%d-%m-%Y %H") }}',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const webTitleTemplateCheatSheet: CheatSheetInterface = {
|
||||
name: 'Web title template cheatsheet',
|
||||
description: 'Jinja2 is used for templating (docs). \n Markdown is used for markup',
|
||||
fields: [
|
||||
{
|
||||
name: 'Markdown refresher',
|
||||
listItems: [
|
||||
{ codeExample: '**bold**, _italic_, >quote, `code`, ```multiline code```, [``](url), - bullet list' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Jinja2 refresher ',
|
||||
listItems: [
|
||||
{ listItemName: ' {{ payload.labels.foo }} - extract field value' },
|
||||
{
|
||||
listItemName: 'Conditions',
|
||||
codeExample: '{%- if "status" in payload %} \n {{ payload.status }} \n {% endif -%}',
|
||||
},
|
||||
{ listItemName: 'Booleans', codeExample: '{{ payload.status == “resolved” }}' },
|
||||
{ listItemName: 'Loops', codeExample: '{% for label in labels %} \n {{ label.title }} \n {% endfor %}' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Additional jinja2 variables',
|
||||
listItems: [
|
||||
{ listItemName: 'payload - payload of last alert in the group' },
|
||||
{ listItemName: 'web_title, web_mesage, web_image_url - templates from Web' },
|
||||
{ listItemName: 'payload, grafana_oncall_link, grafana_oncall_incident_id, integration_name, source_link' },
|
||||
{ listItemName: 'time(), datetimeformat, iso8601_to_time' },
|
||||
{ listItemName: 'to_pretty_json' },
|
||||
{ listItemName: 'regex_replace, regex_match' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Examples',
|
||||
listItems: [
|
||||
{
|
||||
listItemName: 'Show status if exists',
|
||||
codeExample: '{%- if "status" in payload %} \n **Status**: {{ payload.status }} \n {% endif -%}',
|
||||
},
|
||||
{
|
||||
listItemName: 'Show field value or “N/A” is not exist',
|
||||
codeExample: '{{ payload.labels.foo | default(“N/A”) }}',
|
||||
},
|
||||
{
|
||||
listItemName: 'Iterate over labels dictionary',
|
||||
codeExample:
|
||||
'**Labels:** \n {% for k, v in payload["labels"].items() %} \n *{{ k }}*: {{ v }} \n {% endfor %} ',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const slackMessageTemplateCheatSheet: CheatSheetInterface = {
|
||||
name: 'Slack message template cheatsheet',
|
||||
description: 'Jinja2 is used for templating (docs). \n Markdown is used for markup',
|
||||
fields: [
|
||||
{
|
||||
name: 'Slack Markdown refresher',
|
||||
listItems: [
|
||||
{ listItemName: '**bold**, _italic_, >quote, `code`, ```multiline code```, <slug|url> - bullet list' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Jinja2 refresher ',
|
||||
listItems: [
|
||||
{ listItemName: ' {{ payload.labels.foo }} - extract field value' },
|
||||
{
|
||||
listItemName: 'Conditions',
|
||||
codeExample: '{%- if "status" in payload %} \n {{ payload.status }} \n {% endif -%}',
|
||||
},
|
||||
{ listItemName: 'Booleans', codeExample: '{{ payload.status == “resolved” }}' },
|
||||
{ listItemName: 'Loops', codeExample: '{% for label in labels %} \n {{ label.title }} \n {% endfor %}' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Additional jinja2 variables',
|
||||
listItems: [
|
||||
{ listItemName: 'payload - payload of last alert in the group' },
|
||||
{ listItemName: 'web_title, web_mesage, web_image_url - templates from Web' },
|
||||
{ listItemName: 'payload, grafana_oncall_link, grafana_oncall_incident_id, integration_name, source_link' },
|
||||
{ listItemName: 'time(), datetimeformat, iso8601_to_time' },
|
||||
{ listItemName: 'to_pretty_json' },
|
||||
{ listItemName: 'regex_replace, regex_match' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Examples',
|
||||
listItems: [
|
||||
{
|
||||
listItemName: 'Examples Convert Web template in Classic Markdown to Slack markdown',
|
||||
codeExample: '{{ web_message \n| replace("**", "*") \n| regex_replace("/((.*))[(.*)]/", "<$2|$1>") }}',
|
||||
},
|
||||
{
|
||||
listItemName: 'Show status if exists',
|
||||
codeExample: '{%- if "status" in payload %} \n **Status**: {{ payload.status }} \n {% endif -%}',
|
||||
},
|
||||
{
|
||||
listItemName: 'Show field value or “N/A” is not exist',
|
||||
codeExample: '{{ payload.labels.foo | default(“N/A”) }}',
|
||||
},
|
||||
{
|
||||
listItemName: 'Iterate over labels dictionary',
|
||||
codeExample:
|
||||
'**Labels:** \n {% for k, v in payload["labels"].items() %} \n *{{ k }}*: {{ v }} \n {% endfor %} ',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.cheatsheet-container {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.cheatsheet-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.cheatsheet-item-small {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
83
grafana-plugin/src/components/CheatSheet/CheatSheet.tsx
Normal file
83
grafana-plugin/src/components/CheatSheet/CheatSheet.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
|
||||
import { HorizontalGroup, IconButton, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
|
||||
import Block from 'components/GBlock/Block';
|
||||
import Text from 'components/Text/Text';
|
||||
import { openNotification } from 'utils';
|
||||
|
||||
import { CheatSheetInterface, CheatSheetItem } from './CheatSheet.config';
|
||||
|
||||
import styles from './CheatSheet.module.css';
|
||||
|
||||
interface CheatSheetProps {
|
||||
cheatSheetData: CheatSheetInterface;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const CheatSheet = (props: CheatSheetProps) => {
|
||||
const { cheatSheetData, onClose } = props;
|
||||
return (
|
||||
<div className={cx('cheatsheet-container')}>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text.Title level={3}>{cheatSheetData.name}</Text.Title>
|
||||
<IconButton name="times" onClick={onClose} />
|
||||
</HorizontalGroup>
|
||||
<Text type="secondary">{cheatSheetData.description}</Text>
|
||||
<div>
|
||||
{cheatSheetData.fields?.map((field: CheatSheetItem) => {
|
||||
return (
|
||||
<div key={field.name} className={cx('cheatsheet-item')}>
|
||||
<CheatSheetListItem field={field} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface CheatSheetListItemProps {
|
||||
field: CheatSheetItem;
|
||||
}
|
||||
const CheatSheetListItem = (props: CheatSheetListItemProps) => {
|
||||
const { field } = props;
|
||||
return (
|
||||
<>
|
||||
<Text.Title level={4}>{field.name}</Text.Title>
|
||||
{field.listItems?.map((item, key) => {
|
||||
return (
|
||||
<div key={key}>
|
||||
<VerticalGroup spacing="md">
|
||||
{item.listItemName && (
|
||||
<li style={{ margin: '0 0 0 4px' }}>
|
||||
<Text>{item.listItemName}</Text>
|
||||
</li>
|
||||
)}
|
||||
{item.codeExample && (
|
||||
<div className={cx('cheatsheet-item-small')}>
|
||||
<Block bordered fullWidth withBackground>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text type="link">{item.codeExample}</Text>
|
||||
<CopyToClipboard text={item.codeExample} onCopy={() => openNotification('Example copied')}>
|
||||
<IconButton name="copy" />
|
||||
</CopyToClipboard>
|
||||
</HorizontalGroup>
|
||||
</Block>
|
||||
</div>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheatSheet;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
.element {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
padding: 3px 4px;
|
||||
|
||||
&--link,
|
||||
&--warning,
|
||||
&--success {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: var(--tag-background-primary);
|
||||
border: 1px solid var(--tag-border-primary);
|
||||
color: var(--tag-text-primary);
|
||||
}
|
||||
&--warning {
|
||||
background: var(--tag-background-warning);
|
||||
border: 1px solid var(--tag-border-warning);
|
||||
color: var(--tag-text-warning);
|
||||
}
|
||||
&--success {
|
||||
background: var(--tag-background-success);
|
||||
border: 1px solid var(--tag-border-success);
|
||||
color: var(--tag-text-success);
|
||||
}
|
||||
|
||||
&--padding {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
width: auto;
|
||||
}
|
||||
55
grafana-plugin/src/components/CounterBadge/CounterBadge.tsx
Normal file
55
grafana-plugin/src/components/CounterBadge/CounterBadge.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Icon, Tooltip, IconName, VerticalGroup, HorizontalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import Text, { TextType } from 'components/Text/Text';
|
||||
|
||||
import styles from './CounterBadge.module.scss';
|
||||
|
||||
interface CounterBadgeProps {
|
||||
borderType: Partial<TextType>;
|
||||
count: number | string;
|
||||
tooltipTitle: string;
|
||||
tooltipContent: React.ReactNode;
|
||||
|
||||
icon?: string;
|
||||
addPadding?: boolean;
|
||||
|
||||
onHover?: () => void;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const CounterBadge: FC<CounterBadgeProps> = (props) => {
|
||||
const { borderType, count, tooltipTitle, tooltipContent, onHover, addPadding, icon } = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
placement="bottom-start"
|
||||
interactive
|
||||
content={
|
||||
<div className={cx('tooltip')}>
|
||||
<VerticalGroup spacing="xs">
|
||||
<Text type="primary">{tooltipTitle}</Text>
|
||||
{tooltipContent && <Text type="secondary">{tooltipContent}</Text>}
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cx('root', 'element', { [`element--${borderType}`]: true }, { 'element--padding': addPadding })}
|
||||
onMouseEnter={onHover}
|
||||
>
|
||||
<HorizontalGroup spacing="xs">
|
||||
{icon && (
|
||||
<Icon className={cx('element__icon', { [`element__icon--${borderType}`]: true })} name={icon as IconName} />
|
||||
)}
|
||||
<Text className={cx('element__text', { [`element__text--${borderType}`]: true })}>{count}</Text>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default CounterBadge;
|
||||
|
|
@ -29,13 +29,17 @@ const Block: FC<BlockProps> = (props) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className={cx('root', className, {
|
||||
root_bordered: bordered,
|
||||
root_shadowed: shadowed,
|
||||
'root--fullWidth': fullWidth,
|
||||
'root--withBackground': withBackground,
|
||||
'root--hover': hover,
|
||||
})}
|
||||
className={cx(
|
||||
'root',
|
||||
{
|
||||
root_bordered: bordered,
|
||||
root_shadowed: shadowed,
|
||||
'root--fullWidth': fullWidth,
|
||||
'root--withBackGround': withBackground,
|
||||
'root--hover': hover,
|
||||
},
|
||||
className
|
||||
)}
|
||||
style={style}
|
||||
{...rest}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
.integrationTree__container {
|
||||
margin-left: 32px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: calc(100% - 10px);
|
||||
border: var(--border-weak);
|
||||
margin-top: 0px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
}
|
||||
|
||||
.integrationTree__element {
|
||||
visibility: hidden;
|
||||
overflow-y: hidden;
|
||||
height: 0;
|
||||
|
||||
&--visible {
|
||||
visibility: visible;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.integrationTree__group {
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.integrationTree__icon {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
transform: translateY(50%);
|
||||
left: -30px;
|
||||
color: var(--always-gray);
|
||||
width: 25px;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
background-color: var(--primary-background) !important;
|
||||
border: 1px solid var(--primary-background);
|
||||
z-index: 100;
|
||||
border-radius: 4px;
|
||||
padding: 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { IconButton, IconName } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { isArray, isUndefined } from 'lodash-es';
|
||||
|
||||
import styles from './IntegrationCollapsibleTreeView.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
export interface IntegrationCollapsibleItem {
|
||||
customIcon?: IconName;
|
||||
expandedView: React.ReactNode;
|
||||
collapsedView: React.ReactNode;
|
||||
isCollapsible: boolean;
|
||||
}
|
||||
|
||||
interface IntegrationCollapsibleTreeViewProps {
|
||||
configElements: Array<IntegrationCollapsibleItem | IntegrationCollapsibleItem[]>;
|
||||
}
|
||||
|
||||
const IntegrationCollapsibleTreeView: React.FC<IntegrationCollapsibleTreeViewProps> = (props) => {
|
||||
const { configElements } = props;
|
||||
|
||||
const [expandedList, setExpandedList] = useState(getStartingExpandedState());
|
||||
|
||||
return (
|
||||
<div className={cx('integrationTree__container')}>
|
||||
{configElements.map((item: IntegrationCollapsibleItem | IntegrationCollapsibleItem[], idx) => {
|
||||
if (isArray(item)) {
|
||||
return item.map((it, innerIdx) => (
|
||||
<IntegrationCollapsibleTreeItem
|
||||
item={it}
|
||||
key={`${idx}-${innerIdx}`}
|
||||
onClick={() => expandOrCollapseAtPos(idx, innerIdx)}
|
||||
isExpanded={!!expandedList[idx][innerIdx]}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<IntegrationCollapsibleTreeItem
|
||||
item={item}
|
||||
key={idx}
|
||||
onClick={() => expandOrCollapseAtPos(idx)}
|
||||
isExpanded={!!expandedList[idx]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
function getStartingExpandedState(): Array<boolean | boolean[]> {
|
||||
const expandedArrayValues = new Array<boolean | boolean[]>(configElements.length);
|
||||
configElements.forEach((elem, index) => {
|
||||
expandedArrayValues[index] = Array.isArray(elem) ? new Array(elem.length).fill(true) : true;
|
||||
});
|
||||
|
||||
return expandedArrayValues;
|
||||
}
|
||||
|
||||
function expandOrCollapseAtPos(i: number, j: number = undefined) {
|
||||
setExpandedList(
|
||||
expandedList.map((elem, index) => {
|
||||
if (!isUndefined(j) && index === i) {
|
||||
return (elem as boolean[]).map((innerElem: boolean, jIndex: number) =>
|
||||
jIndex === j ? !innerElem : innerElem
|
||||
);
|
||||
}
|
||||
|
||||
return index === i ? !elem : elem;
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const IntegrationCollapsibleTreeItem: React.FC<{ item: IntegrationCollapsibleItem; isExpanded: boolean; onClick }> = ({
|
||||
item,
|
||||
isExpanded,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cx('integrationTree__group')}>
|
||||
<div className={cx('integrationTree__icon')}>
|
||||
<IconButton name={getIconName()} onClick={!item.isCollapsible ? undefined : onClick} size="lg" />
|
||||
</div>
|
||||
<div className={cx('integrationTree__element', { 'integrationTree__element--visible': isExpanded })}>
|
||||
{item.expandedView}
|
||||
</div>
|
||||
<div className={cx('integrationTree__element', { 'integrationTree__element--visible': !isExpanded })}>
|
||||
{item.collapsedView}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function getIconName(): IconName {
|
||||
if (item.customIcon) {
|
||||
return item.customIcon;
|
||||
}
|
||||
return isExpanded ? 'angle-down' : 'angle-right';
|
||||
}
|
||||
};
|
||||
|
||||
export default IntegrationCollapsibleTreeView;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.root {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-right: 24px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.icons {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 6px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
height: 25px;
|
||||
padding-right: 78px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { HorizontalGroup, IconButton, Input } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
|
||||
import { openNotification } from 'utils';
|
||||
|
||||
import styles from './IntegrationMaskedInputField.module.scss';
|
||||
|
||||
interface IntegrationMaskedInputFieldProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const IntegrationMaskedInputField: React.FC<IntegrationMaskedInputFieldProps> = ({ value }) => {
|
||||
const [isMasked, setIsMasked] = useState(true);
|
||||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('input-container')}>{renderInputField()}</div>
|
||||
|
||||
<div className={cx('icons')}>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<IconButton name={'eye'} size={'xs'} onClick={onInputReveal} />
|
||||
<CopyToClipboard text={value} onCopy={onCopy}>
|
||||
<IconButton name={'copy'} size={'xs'} />
|
||||
</CopyToClipboard>
|
||||
<IconButton name={'external-link-alt'} size={'xs'} onClick={onOpen} />
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function renderInputField() {
|
||||
return <Input className={cx('input')} value={isMasked ? value.replace(/./g, '*') : value} disabled />;
|
||||
}
|
||||
|
||||
function onInputReveal() {
|
||||
setIsMasked(!isMasked);
|
||||
}
|
||||
|
||||
function onCopy() {
|
||||
openNotification("Integration's HTTP Endpoint is copied!");
|
||||
}
|
||||
|
||||
function onOpen() {
|
||||
window.open(value, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
export default IntegrationMaskedInputField;
|
||||
|
|
@ -11,9 +11,12 @@ declare const monaco: any;
|
|||
interface MonacoJinja2EditorProps {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
height?: string;
|
||||
data: any;
|
||||
onChange: (value: string) => void;
|
||||
loading: boolean;
|
||||
showLineNumbers?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
loading?: boolean;
|
||||
monacoOptions?: any;
|
||||
}
|
||||
|
||||
const PREDEFINED_TERMS = [
|
||||
|
|
@ -25,7 +28,7 @@ const PREDEFINED_TERMS = [
|
|||
];
|
||||
|
||||
const MonacoJinja2Editor: FC<MonacoJinja2EditorProps> = (props) => {
|
||||
const { value, onChange, disabled, data, loading } = props;
|
||||
const { value, onChange, disabled, data, height, monacoOptions, showLineNumbers = true, loading = false } = props;
|
||||
|
||||
const autoCompleteList = useCallback(
|
||||
() =>
|
||||
|
|
@ -39,7 +42,7 @@ const MonacoJinja2Editor: FC<MonacoJinja2EditorProps> = (props) => {
|
|||
|
||||
const handleMount = useCallback((editor) => {
|
||||
editor.onDidChangeModelContent(() => {
|
||||
onChange(editor.getValue());
|
||||
onChange?.(editor.getValue());
|
||||
});
|
||||
|
||||
editor.focus();
|
||||
|
|
@ -58,13 +61,14 @@ const MonacoJinja2Editor: FC<MonacoJinja2EditorProps> = (props) => {
|
|||
|
||||
return (
|
||||
<CodeEditor
|
||||
monacoOptions={monacoOptions}
|
||||
showMiniMap={false}
|
||||
readOnly={disabled}
|
||||
showLineNumbers
|
||||
showLineNumbers={showLineNumbers}
|
||||
value={value}
|
||||
language="jinja2"
|
||||
width="100%"
|
||||
height="130px"
|
||||
height={height ? `${height}` : `130px`}
|
||||
onEditorDidMount={handleMount}
|
||||
getSuggestions={autoCompleteList}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { OutgoingWebhook2Store } from 'models/outgoing_webhook_2/outgoing_webhoo
|
|||
import { ScheduleStore } from 'models/schedule/schedule';
|
||||
import { WaitDelay } from 'models/wait_delay';
|
||||
import { SelectOption } from 'state/types';
|
||||
import { getVar } from 'utils/DOM';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
import DragHandle from './DragHandle';
|
||||
|
|
@ -38,13 +39,14 @@ const cx = cn.bind(styles);
|
|||
export interface EscalationPolicyProps {
|
||||
data: EscalationPolicyType;
|
||||
waitDelays?: any[];
|
||||
isDisabled?: boolean;
|
||||
numMinutesInWindowOptions: SelectOption[];
|
||||
channels?: any[];
|
||||
onChange: (id: EscalationPolicyType['id'], value: Partial<EscalationPolicyType>) => void;
|
||||
onDelete: (data: EscalationPolicyType) => void;
|
||||
escalationChoices: any[];
|
||||
number: number;
|
||||
color: string;
|
||||
backgroundColor: string;
|
||||
isSlackInstalled: boolean;
|
||||
teamStore: GrafanaTeamStore;
|
||||
outgoingWebhookStore: OutgoingWebhookStore;
|
||||
|
|
@ -54,7 +56,7 @@ export interface EscalationPolicyProps {
|
|||
|
||||
export class EscalationPolicy extends React.Component<EscalationPolicyProps, any> {
|
||||
render() {
|
||||
const { data, escalationChoices, number, color } = this.props;
|
||||
const { data, escalationChoices, number, backgroundColor, isDisabled } = this.props;
|
||||
const { id, step, is_final } = data;
|
||||
|
||||
const escalationOption = escalationChoices.find(
|
||||
|
|
@ -62,14 +64,20 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
);
|
||||
|
||||
return (
|
||||
<Timeline.Item key={id} contentClassName={cx('root')} number={number} color={color}>
|
||||
<Timeline.Item
|
||||
key={id}
|
||||
contentClassName={cx('root')}
|
||||
number={number}
|
||||
textColor={isDisabled ? getVar('--tag-text-success') : undefined}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
<WithPermissionControlTooltip disableByPaywall userAction={UserActions.EscalationChainsWrite}>
|
||||
<DragHandle />
|
||||
</WithPermissionControlTooltip>
|
||||
{escalationOption &&
|
||||
reactStringReplace(escalationOption.display_name, /\{\{([^}]+)\}\}/g, this.replacePlaceholder)}
|
||||
{this._renderNote()}
|
||||
{is_final ? null : (
|
||||
{is_final || isDisabled ? null : (
|
||||
<WithPermissionControlTooltip className={cx('delete')} userAction={UserActions.EscalationChainsWrite}>
|
||||
<IconButton
|
||||
name="trash-alt"
|
||||
|
|
@ -142,7 +150,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
}
|
||||
|
||||
private _renderNotifyToUsersQueue() {
|
||||
const { data } = this.props;
|
||||
const { data, isDisabled } = this.props;
|
||||
const { notify_to_users_queue } = data;
|
||||
|
||||
return (
|
||||
|
|
@ -155,6 +163,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
isMulti
|
||||
showSearch
|
||||
allowClear
|
||||
disabled={isDisabled}
|
||||
modelName="userStore"
|
||||
displayField="username"
|
||||
valueField="pk"
|
||||
|
|
@ -170,7 +179,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
}
|
||||
|
||||
private renderImportance() {
|
||||
const { data } = this.props;
|
||||
const { data, isDisabled } = this.props;
|
||||
const { important } = data;
|
||||
|
||||
return (
|
||||
|
|
@ -178,6 +187,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
<Select
|
||||
menuShouldPortal
|
||||
className={cx('select', 'control')}
|
||||
disabled={isDisabled}
|
||||
value={Number(important)}
|
||||
// @ts-ignore
|
||||
onChange={this._getOnSelectChangeHandler('important')}
|
||||
|
|
@ -214,13 +224,14 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
}
|
||||
|
||||
private renderTimeRange() {
|
||||
const { data } = this.props;
|
||||
const { data, isDisabled } = this.props;
|
||||
|
||||
return (
|
||||
<WithPermissionControlTooltip key="time-range" disableByPaywall userAction={UserActions.EscalationChainsWrite}>
|
||||
<TimeRange
|
||||
from={data.from_time}
|
||||
to={data.to_time}
|
||||
disabled={isDisabled}
|
||||
onChange={this._getOnTimeRangeChangeHandler()}
|
||||
className={cx('select', 'control')}
|
||||
/>
|
||||
|
|
@ -229,13 +240,14 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
}
|
||||
|
||||
private _renderWaitDelays() {
|
||||
const { data, waitDelays = [] } = this.props;
|
||||
const { data, isDisabled, waitDelays = [] } = this.props;
|
||||
const { wait_delay } = data;
|
||||
|
||||
return (
|
||||
<WithPermissionControlTooltip key="wait-delay" disableByPaywall userAction={UserActions.EscalationChainsWrite}>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
disabled={isDisabled}
|
||||
placeholder="Select Wait Delay"
|
||||
className={cx('select', 'control')}
|
||||
// @ts-ignore
|
||||
|
|
@ -252,7 +264,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
}
|
||||
|
||||
private renderNumAlertsInWindow() {
|
||||
const { data } = this.props;
|
||||
const { data, isDisabled } = this.props;
|
||||
const { num_alerts_in_window } = data;
|
||||
|
||||
return (
|
||||
|
|
@ -263,6 +275,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
>
|
||||
<Input
|
||||
placeholder="Count"
|
||||
disabled={isDisabled}
|
||||
className={cx('control')}
|
||||
value={num_alerts_in_window}
|
||||
onChange={this._getOnInputChangeHandler('num_alerts_in_window')}
|
||||
|
|
@ -278,7 +291,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
}
|
||||
|
||||
private renderNumMinutesInWindowOptions() {
|
||||
const { data, numMinutesInWindowOptions = [] } = this.props;
|
||||
const { data, isDisabled, numMinutesInWindowOptions = [] } = this.props;
|
||||
const { num_minutes_in_window } = data;
|
||||
|
||||
return (
|
||||
|
|
@ -289,6 +302,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
>
|
||||
<Select
|
||||
menuShouldPortal
|
||||
disabled={isDisabled}
|
||||
placeholder="Period"
|
||||
className={cx('select', 'control')}
|
||||
// @ts-ignore
|
||||
|
|
@ -304,7 +318,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
}
|
||||
|
||||
private _renderNotifySchedule() {
|
||||
const { data, teamStore, scheduleStore } = this.props;
|
||||
const { data, isDisabled, teamStore, scheduleStore } = this.props;
|
||||
const { notify_schedule } = data;
|
||||
|
||||
return (
|
||||
|
|
@ -316,6 +330,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
<GSelect
|
||||
showSearch
|
||||
allowClear
|
||||
disabled={isDisabled}
|
||||
modelName="scheduleStore"
|
||||
displayField="name"
|
||||
valueField="id"
|
||||
|
|
@ -338,7 +353,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
}
|
||||
|
||||
private _renderNotifyUserGroup() {
|
||||
const { data } = this.props;
|
||||
const { data, isDisabled } = this.props;
|
||||
const { notify_to_group } = data;
|
||||
|
||||
return (
|
||||
|
|
@ -348,6 +363,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
userAction={UserActions.EscalationChainsWrite}
|
||||
>
|
||||
<GSelect
|
||||
disabled={isDisabled}
|
||||
modelName="userGroupStore"
|
||||
displayField="name"
|
||||
valueField="id"
|
||||
|
|
@ -362,13 +378,14 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
}
|
||||
|
||||
private _renderTriggerCustomAction() {
|
||||
const { data, teamStore, outgoingWebhookStore } = this.props;
|
||||
const { data, isDisabled, teamStore, outgoingWebhookStore } = this.props;
|
||||
const { custom_button_trigger } = data;
|
||||
|
||||
return (
|
||||
<WithPermissionControlTooltip key="custom-button" disableByPaywall userAction={UserActions.EscalationChainsWrite}>
|
||||
<GSelect
|
||||
showSearch
|
||||
disabled={isDisabled}
|
||||
modelName="outgoingWebhookStore"
|
||||
displayField="name"
|
||||
valueField="id"
|
||||
|
|
@ -392,7 +409,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
}
|
||||
|
||||
private _renderTriggerCustomWebhook() {
|
||||
const { data, teamStore, outgoingWebhook2Store } = this.props;
|
||||
const { data, isDisabled, teamStore, outgoingWebhook2Store } = this.props;
|
||||
const { custom_webhook } = data;
|
||||
|
||||
return (
|
||||
|
|
@ -403,6 +420,7 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
>
|
||||
<GSelect
|
||||
showSearch
|
||||
disabled={isDisabled}
|
||||
modelName="outgoingWebhook2Store"
|
||||
displayField="name"
|
||||
valueField="id"
|
||||
|
|
@ -489,4 +507,4 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
|||
};
|
||||
}
|
||||
|
||||
export default SortableElement(EscalationPolicy);
|
||||
export default SortableElement(EscalationPolicy) as React.ComponentClass<EscalationPolicyProps>;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export class NotificationPolicy extends React.Component<NotificationPolicyProps,
|
|||
const { id, step } = data;
|
||||
|
||||
return (
|
||||
<Timeline.Item className={cx('root')} number={number} color={color}>
|
||||
<Timeline.Item className={cx('root')} number={number} backgroundColor={color}>
|
||||
<div className={cx('step')}>
|
||||
<WithPermissionControlTooltip disableByPaywall userAction={userAction}>
|
||||
<DragHandle />
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import React, { FC, useEffect, useState } from 'react';
|
|||
import { Tooltip, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import CounterBadge from 'components/CounterBadge/CounterBadge';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import { ScheduleQualityDetails } from 'components/ScheduleQualityDetails/ScheduleQualityDetails';
|
||||
import StatusCounterBadgeWithTooltip from 'components/StatusCounterBadgeWithTooltip/StatusCounterBadgeWithTooltip';
|
||||
import Tag from 'components/Tag/Tag';
|
||||
import Text from 'components/Text/Text';
|
||||
import { Schedule, ScheduleScoreQualityResponse, ScheduleScoreQualityResult } from 'models/schedule/schedule.types';
|
||||
|
|
@ -40,8 +40,8 @@ const ScheduleQuality: FC<ScheduleQualityProps> = ({ schedule, lastUpdated }) =>
|
|||
<>
|
||||
<div className={cx('root')}>
|
||||
{relatedEscalationChains?.length > 0 && schedule?.number_of_escalation_chains > 0 && (
|
||||
<StatusCounterBadgeWithTooltip
|
||||
type="link"
|
||||
<CounterBadge
|
||||
borderType="link"
|
||||
addPadding
|
||||
count={schedule.number_of_escalation_chains}
|
||||
tooltipTitle="Used in escalations"
|
||||
|
|
@ -60,8 +60,8 @@ const ScheduleQuality: FC<ScheduleQualityProps> = ({ schedule, lastUpdated }) =>
|
|||
)}
|
||||
|
||||
{schedule.warnings?.length > 0 && (
|
||||
<StatusCounterBadgeWithTooltip
|
||||
type="warning"
|
||||
<CounterBadge
|
||||
borderType="warning"
|
||||
addPadding
|
||||
count={schedule.warnings.length}
|
||||
tooltipTitle="Warnings"
|
||||
|
|
|
|||
|
|
@ -37,13 +37,6 @@ $width: 340px;
|
|||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.line-break {
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--always-gray);
|
||||
margin-top: 8px;
|
||||
opacity: 15%;
|
||||
}
|
||||
|
||||
.metholodogy {
|
||||
padding: 4px 0px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export const ScheduleQualityDetails: FC<ScheduleQualityDetailsProps> = ({ qualit
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className={cx('line-break')} />
|
||||
<div className="thin-line-break" />
|
||||
|
||||
<div className={cx('container', 'container--withTopPadding', 'container--withLateralPadding')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
|
|
|
|||
|
|
@ -1,449 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SourceCode It renders at 0% 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="c-progressBar__wrapper"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--danger"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--danger"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--danger"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--danger"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--danger"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SourceCode It renders at 25% 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="c-progressBar__wrapper"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 25%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SourceCode It renders at 30% 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="c-progressBar__wrapper"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 50%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SourceCode It renders at 50% 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="c-progressBar__wrapper"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 50%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--warning"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SourceCode It renders at 65% 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="c-progressBar__wrapper"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 25%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SourceCode It renders at 70% 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="c-progressBar__wrapper"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 50%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SourceCode It renders at 100% 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="c-progressBar__wrapper"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c-progressBar__row c-progressBar__row--progress"
|
||||
data-testid="progressBar__row"
|
||||
style="width: 20%;"
|
||||
>
|
||||
<div
|
||||
class="c-progressBar__bar c-progressBar__bar--primary"
|
||||
data-testid="progressBar__bar"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -15,10 +15,11 @@ interface SourceCodeProps {
|
|||
showClipboardIconOnly?: boolean;
|
||||
showCopyToClipboard?: boolean;
|
||||
children?: any;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SourceCode: FC<SourceCodeProps> = (props) => {
|
||||
const { children, noMaxHeight = false, showClipboardIconOnly = false, showCopyToClipboard = true } = props;
|
||||
const { children, noMaxHeight = false, showClipboardIconOnly = false, showCopyToClipboard = true, className } = props;
|
||||
const showClipboardCopy = showClipboardIconOnly || showCopyToClipboard;
|
||||
|
||||
return (
|
||||
|
|
@ -48,9 +49,13 @@ const SourceCode: FC<SourceCodeProps> = (props) => {
|
|||
</CopyToClipboard>
|
||||
)}
|
||||
<pre
|
||||
className={cx('scroller', {
|
||||
'scroller--maxHeight': !noMaxHeight,
|
||||
})}
|
||||
className={cx(
|
||||
'scroller',
|
||||
{
|
||||
'scroller--maxHeight': !noMaxHeight,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<code>{children}</code>
|
||||
</pre>
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
.element {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
padding: 3px 4px;
|
||||
|
||||
&--link {
|
||||
background: rgba(27, 133, 94, 0.15);
|
||||
border: 1px solid var(--tag-border-success);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: rgba(245, 183, 61, 0.18);
|
||||
border: 1px solid var(--tag-border-warning);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&--padding {
|
||||
padding: 3px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.element__text--link,
|
||||
.element__icon--link {
|
||||
color: var(--tag-text-success);
|
||||
}
|
||||
|
||||
.element__text--warning,
|
||||
.element__icon--warning {
|
||||
color: var(--tag-text-warning);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
width: auto;
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { HorizontalGroup, VerticalGroup, Icon, Tooltip, IconName } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import Text, { TextType } from 'components/Text/Text';
|
||||
|
||||
import styles from './StatusCounterBadgeWithTooltip.module.scss';
|
||||
|
||||
interface StatusCounterBadgeWithTooltipProps {
|
||||
type: Partial<TextType>;
|
||||
count: number;
|
||||
tooltipTitle: string;
|
||||
tooltipContent: React.ReactNode;
|
||||
addPadding?: boolean;
|
||||
onHover?: () => void;
|
||||
}
|
||||
|
||||
const typeToIcon = {
|
||||
link: 'link',
|
||||
warning: 'exclamation-triangle',
|
||||
};
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
const StatusCounterBadgeWithTooltip: FC<StatusCounterBadgeWithTooltipProps> = (props) => {
|
||||
const { type, count, tooltipTitle, tooltipContent, onHover, addPadding } = props;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
placement="bottom-start"
|
||||
interactive
|
||||
content={
|
||||
<div className={cx('tooltip')}>
|
||||
<VerticalGroup>
|
||||
<Text type="secondary">{tooltipTitle}</Text>
|
||||
<Text type="secondary">{tooltipContent}</Text>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cx('root', 'element', { [`element--${type}`]: true }, { 'element--padding': addPadding })}
|
||||
onMouseEnter={onHover}
|
||||
>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Icon
|
||||
className={cx('element__icon', { [`element__icon--${type}`]: true })}
|
||||
name={typeToIcon[type] as IconName}
|
||||
/>
|
||||
<Text className={cx('element__text', { [`element__text--${type}`]: true })}>{count}</Text>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusCounterBadgeWithTooltip;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
.root {
|
||||
border-radius: 2px;
|
||||
padding: 1px 7px 4px 7px;
|
||||
line-height: 100%;
|
||||
padding: 4px 7px;
|
||||
color: white;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ const cx = cn.bind(styles);
|
|||
export interface TimelineItemProps {
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
color?: string;
|
||||
isDisabled?: boolean;
|
||||
backgroundColor?: string;
|
||||
textColor?: string;
|
||||
number?: number;
|
||||
badge?: number;
|
||||
children?: any;
|
||||
|
|
@ -19,15 +21,21 @@ const TimelineItem: React.FC<TimelineItemProps> = ({
|
|||
className,
|
||||
contentClassName,
|
||||
children,
|
||||
color = '#3274D9',
|
||||
isDisabled,
|
||||
backgroundColor = '#3274D9',
|
||||
textColor = '#ffffff',
|
||||
number,
|
||||
}) => (
|
||||
<li className={cx('item', className)}>
|
||||
<div className={cx('dot')} style={{ backgroundColor: color }}>
|
||||
{number}
|
||||
</div>
|
||||
<div className={cx('content', contentClassName)}>{children}</div>
|
||||
</li>
|
||||
);
|
||||
}) => {
|
||||
return (
|
||||
<li className={cx('item', className)}>
|
||||
{!isDisabled && (
|
||||
<div className={cx('dot')} style={{ backgroundColor, color: textColor }}>
|
||||
{number}
|
||||
</div>
|
||||
)}
|
||||
<div className={cx('content', contentClassName)}>{children}</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineItem;
|
||||
|
|
|
|||
|
|
@ -1,241 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Unauthorized renders properly - access control enabled: false 1`] = `
|
||||
<div
|
||||
className="not-found"
|
||||
>
|
||||
<div
|
||||
className="css-9ztktj-vertical-group"
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="css-1ec9088-layoutChildrenWrapper"
|
||||
>
|
||||
<h1
|
||||
className="title error-code"
|
||||
>
|
||||
<span
|
||||
className="root text text--undefined text--medium"
|
||||
>
|
||||
403
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="css-1ec9088-layoutChildrenWrapper"
|
||||
>
|
||||
<h4
|
||||
className="title"
|
||||
>
|
||||
<span
|
||||
className="root text text--undefined text--medium"
|
||||
>
|
||||
You do not have access to view this page.
|
||||
|
||||
You must be at least an Admin.
|
||||
<br />
|
||||
<br />
|
||||
Please contact your organization administrator to request access.
|
||||
</span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Unauthorized renders properly - access control enabled: true 1`] = `
|
||||
<div
|
||||
className="not-found"
|
||||
>
|
||||
<div
|
||||
className="css-9ztktj-vertical-group"
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="css-1ec9088-layoutChildrenWrapper"
|
||||
>
|
||||
<h1
|
||||
className="title error-code"
|
||||
>
|
||||
<span
|
||||
className="root text text--undefined text--medium"
|
||||
>
|
||||
403
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="css-1ec9088-layoutChildrenWrapper"
|
||||
>
|
||||
<h4
|
||||
className="title"
|
||||
>
|
||||
<span
|
||||
className="root text text--undefined text--medium"
|
||||
>
|
||||
You do not have access to view this page.
|
||||
|
||||
You are missing the grafana-oncall-app.testing:hi permission.
|
||||
<br />
|
||||
<br />
|
||||
Please contact your organization administrator to request access.
|
||||
</span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Unauthorized renders properly the grammar for different roles - Admin 1`] = `
|
||||
<div
|
||||
className="not-found"
|
||||
>
|
||||
<div
|
||||
className="css-9ztktj-vertical-group"
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="css-1ec9088-layoutChildrenWrapper"
|
||||
>
|
||||
<h1
|
||||
className="title error-code"
|
||||
>
|
||||
<span
|
||||
className="root text text--undefined text--medium"
|
||||
>
|
||||
403
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="css-1ec9088-layoutChildrenWrapper"
|
||||
>
|
||||
<h4
|
||||
className="title"
|
||||
>
|
||||
<span
|
||||
className="root text text--undefined text--medium"
|
||||
>
|
||||
You do not have access to view this page.
|
||||
|
||||
You must be at least an Admin.
|
||||
<br />
|
||||
<br />
|
||||
Please contact your organization administrator to request access.
|
||||
</span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Unauthorized renders properly the grammar for different roles - Editor 1`] = `
|
||||
<div
|
||||
className="not-found"
|
||||
>
|
||||
<div
|
||||
className="css-9ztktj-vertical-group"
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="css-1ec9088-layoutChildrenWrapper"
|
||||
>
|
||||
<h1
|
||||
className="title error-code"
|
||||
>
|
||||
<span
|
||||
className="root text text--undefined text--medium"
|
||||
>
|
||||
403
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="css-1ec9088-layoutChildrenWrapper"
|
||||
>
|
||||
<h4
|
||||
className="title"
|
||||
>
|
||||
<span
|
||||
className="root text text--undefined text--medium"
|
||||
>
|
||||
You do not have access to view this page.
|
||||
|
||||
You must be at least an Editor.
|
||||
<br />
|
||||
<br />
|
||||
Please contact your organization administrator to request access.
|
||||
</span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Unauthorized renders properly the grammar for different roles - Viewer 1`] = `
|
||||
<div
|
||||
className="not-found"
|
||||
>
|
||||
<div
|
||||
className="css-9ztktj-vertical-group"
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="css-1ec9088-layoutChildrenWrapper"
|
||||
>
|
||||
<h1
|
||||
className="title error-code"
|
||||
>
|
||||
<span
|
||||
className="root text text--undefined text--medium"
|
||||
>
|
||||
403
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="css-1ec9088-layoutChildrenWrapper"
|
||||
>
|
||||
<h4
|
||||
className="title"
|
||||
>
|
||||
<span
|
||||
className="root text text--undefined text--medium"
|
||||
>
|
||||
You do not have access to view this page.
|
||||
|
||||
You must be at least a Viewer.
|
||||
<br />
|
||||
<br />
|
||||
Please contact your organization administrator to request access.
|
||||
</span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -4,7 +4,7 @@ import { ContextMenu } from '@grafana/ui';
|
|||
|
||||
export interface WithContextMenuProps {
|
||||
children: (props: { openMenu: React.MouseEventHandler<HTMLElement> }) => JSX.Element;
|
||||
renderMenuItems: () => React.ReactNode;
|
||||
renderMenuItems: ({ closeMenu }: { closeMenu?: () => void }) => React.ReactNode;
|
||||
forceIsOpen?: boolean;
|
||||
focusOnOpen?: boolean;
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ export const WithContextMenu: React.FC<WithContextMenuProps> = ({
|
|||
onClose={() => setIsMenuOpen(false)}
|
||||
x={menuPosition.x}
|
||||
y={menuPosition.y}
|
||||
renderMenuItems={renderMenuItems}
|
||||
renderMenuItems={() => renderMenuItems({ closeMenu: () => setIsMenuOpen(false) })}
|
||||
focusOnOpen={focusOnOpen}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export const ChatOpsConnectors = (props: ChatOpsConnectorsProps) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Timeline.Item number={0} color={getVar('--tag-secondary')}>
|
||||
<Timeline.Item number={0} backgroundColor={getVar('--tag-secondary')}>
|
||||
<VerticalGroup>
|
||||
{isSlackInstalled && <SlackConnector channelFilterId={channelFilterId} />}
|
||||
{isTelegramInstalled && <TelegramConnector channelFilterId={channelFilterId} />}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,13 @@ const cx = cn.bind(styles);
|
|||
|
||||
interface EscalationChainStepsProps {
|
||||
id: EscalationChain['id'];
|
||||
isDisabled?: boolean;
|
||||
addonBefore?: ReactElement;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
const EscalationChainSteps = observer((props: EscalationChainStepsProps) => {
|
||||
const { id, offset = 0, addonBefore } = props;
|
||||
const { id, offset = 0, isDisabled = false, addonBefore } = props;
|
||||
|
||||
const store = useStore();
|
||||
|
||||
|
|
@ -76,11 +77,9 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => {
|
|||
return (
|
||||
<EscalationPolicy
|
||||
key={`item-${escalationPolicy.id}`}
|
||||
index={index}
|
||||
// @ts-ignore
|
||||
data={escalationPolicy}
|
||||
number={index + offset + 1}
|
||||
color={STEP_COLORS[index] || COLOR_RED}
|
||||
backgroundColor={isDisabled ? getVar('--tag-background-success') : STEP_COLORS[index] || COLOR_RED}
|
||||
escalationChoices={escalationPolicyStore.webEscalationChoices}
|
||||
waitDelays={get(escalationPolicyStore.escalationChoices, 'wait_delay.choices', [])}
|
||||
numMinutesInWindowOptions={escalationPolicyStore.numMinutesInWindowOptions}
|
||||
|
|
@ -91,27 +90,34 @@ const EscalationChainSteps = observer((props: EscalationChainStepsProps) => {
|
|||
scheduleStore={store.scheduleStore}
|
||||
outgoingWebhookStore={store.outgoingWebhookStore}
|
||||
outgoingWebhook2Store={store.outgoingWebhook2Store}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
)}
|
||||
<Timeline.Item number={(escalationPolicyIds?.length || 0) + offset + 1} color={getVar('--tag-secondary')}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.EscalationChainsWrite}>
|
||||
<Select
|
||||
isSearchable
|
||||
menuShouldPortal
|
||||
placeholder="Add escalation step..."
|
||||
onChange={handleCreateEscalationStep}
|
||||
options={escalationPolicyStore.webEscalationChoices.map((choice: EscalationPolicyOption) => ({
|
||||
value: choice.value,
|
||||
label: choice.create_display_name,
|
||||
}))}
|
||||
value={null}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</Timeline.Item>
|
||||
{!isDisabled && (
|
||||
<Timeline.Item
|
||||
number={(escalationPolicyIds?.length || 0) + offset + 1}
|
||||
backgroundColor={isDisabled ? getVar('--tag-background-success') : getVar('--tag-secondary')}
|
||||
textColor={isDisabled ? getVar('--tag-text-success') : undefined}
|
||||
>
|
||||
<WithPermissionControlTooltip userAction={UserActions.EscalationChainsWrite}>
|
||||
<Select
|
||||
isSearchable
|
||||
menuShouldPortal
|
||||
placeholder="Add escalation step..."
|
||||
onChange={handleCreateEscalationStep}
|
||||
options={escalationPolicyStore.webEscalationChoices.map((choice: EscalationPolicyOption) => ({
|
||||
value: choice.value,
|
||||
label: choice.create_display_name,
|
||||
}))}
|
||||
value={null}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
</Timeline.Item>
|
||||
)}
|
||||
</SortableList>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
.root {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.root {
|
||||
min-width: 200px;
|
||||
|
||||
& > div {
|
||||
// If not set then inner div will not benefit of min-width
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,13 +8,13 @@ import { observer } from 'mobx-react';
|
|||
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './GSelect.module.css';
|
||||
// import { debounce } from 'lodash';
|
||||
import styles from './GSelect.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface GSelectProps {
|
||||
placeholder: string;
|
||||
isLoading?: boolean;
|
||||
value?: string | string[] | null;
|
||||
defaultValue?: string | string[] | null;
|
||||
onChange: (value: string, item: any) => void;
|
||||
|
|
@ -45,6 +45,7 @@ const GSelect = observer((props: GSelectProps) => {
|
|||
autoFocus,
|
||||
showSearch = false,
|
||||
allowClear = false,
|
||||
isLoading,
|
||||
defaultOpen,
|
||||
placeholder,
|
||||
className,
|
||||
|
|
@ -157,6 +158,7 @@ const GSelect = observer((props: GSelectProps) => {
|
|||
onChange={onChangeCallback}
|
||||
defaultOptions={!disabled}
|
||||
loadOptions={loadOptions}
|
||||
isLoading={isLoading}
|
||||
// @ts-ignore
|
||||
value={values}
|
||||
defaultValue={defaultValue}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { FormItem, FormItemType } from 'components/GForm/GForm.types';
|
||||
|
||||
export const form: { name: string; fields: FormItem[] } = {
|
||||
name: 'Integration',
|
||||
fields: [
|
||||
{
|
||||
name: 'verbal_name',
|
||||
type: FormItemType.Input,
|
||||
validation: { required: true },
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: FormItemType.TextArea,
|
||||
},
|
||||
{
|
||||
name: 'team',
|
||||
label: 'Assign to team',
|
||||
description:
|
||||
'Assigning to the teams allows you to filter Integrations and configure their visibility. Go to OnCall -> Settings -> Team and Access Settings for more details',
|
||||
type: FormItemType.GSelect,
|
||||
extra: {
|
||||
modelName: 'grafanaTeamStore',
|
||||
displayField: 'name',
|
||||
valueField: 'id',
|
||||
showSearch: true,
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { AlertReceiveChannel } from 'models/alert_receive_channel';
|
||||
|
||||
export function prepareForEdit(item: AlertReceiveChannel) {
|
||||
return {
|
||||
verbal_name: item.verbal_name,
|
||||
// description: item.description,
|
||||
team: item.team,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
.content {
|
||||
margin: 4px 4px 50px 4px;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
overflow: auto;
|
||||
scroll-snap-type: y mandatory;
|
||||
padding: 0 10px 10px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cards_centered {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 48%;
|
||||
height: 88px;
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: normal;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.card_featured {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tag {
|
||||
top: 28px;
|
||||
right: 28px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 10px 0 10px 0;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.search-integration {
|
||||
width: 400px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.collapsable-content {
|
||||
width: 100%;
|
||||
background-color: var(--background-secondary);
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.integration-info-list {
|
||||
list-style-position: inside;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.integration-info-item {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
import React, { useState, useCallback, ChangeEvent } from 'react';
|
||||
|
||||
import { Drawer, VerticalGroup, HorizontalGroup, Input, Tag, EmptySearchResult, Button } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Collapse from 'components/Collapse/Collapse';
|
||||
import Block from 'components/GBlock/Block';
|
||||
import GForm from 'components/GForm/GForm';
|
||||
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
|
||||
import Text from 'components/Text/Text';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel';
|
||||
import { AlertReceiveChannelOption } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
import { form } from './IntegrationForm.config';
|
||||
import { prepareForEdit } from './IntegrationForm.helpers';
|
||||
|
||||
import styles from './IntegrationForm.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface IntegrationFormProps {
|
||||
id: AlertReceiveChannel['id'] | 'new';
|
||||
onHide: () => void;
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
const IntegrationForm = observer((props: IntegrationFormProps) => {
|
||||
const { id, onHide, onUpdate } = props;
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const { alertReceiveChannelStore, userStore } = store;
|
||||
|
||||
const user = userStore.currentUser;
|
||||
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
const [showNewIntegrationForm, setShowNewIntegrationForm] = useState(false);
|
||||
// const [showIntegrationListForm, setShowIntegrationListForm] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState<AlertReceiveChannelOption>(undefined);
|
||||
|
||||
const data =
|
||||
id === 'new'
|
||||
? { integration: selectedOption?.value, team: user.current_team }
|
||||
: prepareForEdit(alertReceiveChannelStore.items[id]);
|
||||
|
||||
const integration = alertReceiveChannelStore.getIntegration(data);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(data: Partial<AlertReceiveChannel>) => {
|
||||
(id === 'new' ? alertReceiveChannelStore.create(data) : alertReceiveChannelStore.update(id, data)).then(() => {
|
||||
onHide();
|
||||
onUpdate();
|
||||
});
|
||||
},
|
||||
[id]
|
||||
);
|
||||
|
||||
const handleNewIntegrationOptionSelectCallback = useCallback((option: AlertReceiveChannelOption) => {
|
||||
return () => {
|
||||
setSelectedOption(option);
|
||||
setShowNewIntegrationForm(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleChangeFilter = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterValue(e.currentTarget.value);
|
||||
}, []);
|
||||
|
||||
const { alertReceiveChannelOptions } = alertReceiveChannelStore;
|
||||
|
||||
const options = alertReceiveChannelOptions
|
||||
? alertReceiveChannelOptions.filter((option: AlertReceiveChannelOption) =>
|
||||
option.display_name.toLowerCase().includes(filterValue.toLowerCase())
|
||||
)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{id === 'new' && (
|
||||
<Drawer scrollableContent title="New Integration" onClose={onHide} closeOnMaskClick={false}>
|
||||
<div className={cx('content')}>
|
||||
<VerticalGroup>
|
||||
<div className={cx('search-integration')}>
|
||||
<Input
|
||||
autoFocus
|
||||
value={filterValue}
|
||||
placeholder="Search integrations ..."
|
||||
onChange={handleChangeFilter}
|
||||
/>
|
||||
</div>
|
||||
<div className={cx('cards')} data-testid="create-integration-modal">
|
||||
{options.length ? (
|
||||
options.map((alertReceiveChannelChoice) => {
|
||||
return (
|
||||
<Block
|
||||
bordered
|
||||
shadowed
|
||||
onClick={handleNewIntegrationOptionSelectCallback(alertReceiveChannelChoice)}
|
||||
key={alertReceiveChannelChoice.value}
|
||||
className={cx('card', { card_featured: alertReceiveChannelChoice.featured })}
|
||||
>
|
||||
<div className={cx('card-bg')}>
|
||||
<IntegrationLogo integration={alertReceiveChannelChoice} scale={0.2} />
|
||||
</div>
|
||||
<div className={cx('title')}>
|
||||
<VerticalGroup spacing="none">
|
||||
<Text strong data-testid="integration-display-name">
|
||||
{alertReceiveChannelChoice.display_name}
|
||||
</Text>
|
||||
<Text type="secondary" size="small">
|
||||
{alertReceiveChannelChoice.short_description}
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
{alertReceiveChannelChoice.featured && (
|
||||
<Tag name="Quick connect" className={cx('tag')} colorIndex={7} />
|
||||
)}
|
||||
</Block>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<EmptySearchResult>Could not find anything matching your query</EmptySearchResult>
|
||||
)}
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
</Drawer>
|
||||
)}
|
||||
{(showNewIntegrationForm || id !== 'new') && (
|
||||
<Drawer
|
||||
scrollableContent
|
||||
title={
|
||||
id === 'new'
|
||||
? `New ${selectedOption?.display_name} integration`
|
||||
: `Edit ${integration?.display_name} integration`
|
||||
}
|
||||
onClose={onHide}
|
||||
closeOnMaskClick={false}
|
||||
>
|
||||
<div className={cx('content')}>
|
||||
<VerticalGroup>
|
||||
<GForm form={form} data={data} onSubmit={handleSubmit} />
|
||||
<Collapse
|
||||
headerWithBackground
|
||||
className={cx('collapse')}
|
||||
isOpen={false}
|
||||
label={<Text type="link">How the integration works</Text>}
|
||||
contentClassName={cx('collapsable-content')}
|
||||
>
|
||||
<Text type="secondary">
|
||||
The integration will generate the following:
|
||||
<ul className={cx('integration-info-list')}>
|
||||
<li className={cx('integration-info-item')}>Unique URL endpoint for receiving alerts </li>
|
||||
<li className={cx('integration-info-item')}>
|
||||
Templates to interpret alerts, tailored for Grafana Alerting{' '}
|
||||
</li>
|
||||
<li className={cx('integration-info-item')}>Grafana Alerting contact point </li>
|
||||
<li className={cx('integration-info-item')}>Grafana Alerting notification</li>
|
||||
</ul>
|
||||
What you’ll need to do next:
|
||||
<ul className={cx('integration-info-list')}>
|
||||
<li className={cx('integration-info-item')}>
|
||||
Finish connecting Monitoring system using Unique URL that will be provided on the next step{' '}
|
||||
</li>
|
||||
<li className={cx('integration-info-item')}>
|
||||
Set up routes that are based on alert content, such as severity, region, and service{' '}
|
||||
</li>
|
||||
<li className={cx('integration-info-item')}>Connect escalation chains to the routes</li>
|
||||
<li className={cx('integration-info-item')}>
|
||||
Review templates and personalize according to your requirements
|
||||
</li>
|
||||
</ul>
|
||||
</Text>
|
||||
</Collapse>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
{id === 'new' ? (
|
||||
<Button variant="secondary" onClick={() => setShowNewIntegrationForm(false)}>
|
||||
Back
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<WithPermissionControlTooltip userAction={UserActions.SchedulesWrite}>
|
||||
<Button form={form.name} type="submit">
|
||||
{id === 'new' ? 'Create' : 'Update'} Integration
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
</Drawer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default IntegrationForm;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
.title-container {
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border: var(--border-weak);
|
||||
}
|
||||
|
||||
.template-block-title {
|
||||
padding: 16px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.template-editor-block-title {
|
||||
padding: 16px;
|
||||
align-items: baseline;
|
||||
border: var(--border-weak);
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.template-block-list {
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.template-block-codeeditor {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.template-block-result {
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.result {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.block-style {
|
||||
border: var(--border-weak);
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Drawer, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { TemplateForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config';
|
||||
import CheatSheet from 'components/CheatSheet/CheatSheet';
|
||||
import {
|
||||
groupingTemplateCheatSheet,
|
||||
slackMessageTemplateCheatSheet,
|
||||
webTitleTemplateCheatSheet,
|
||||
} from 'components/CheatSheet/CheatSheet.config';
|
||||
import Block from 'components/GBlock/Block';
|
||||
import MonacoJinja2Editor from 'components/MonacoJinja2Editor/MonacoJinja2Editor';
|
||||
import Text from 'components/Text/Text';
|
||||
import TemplatePreview from 'containers/TemplatePreview/TemplatePreview';
|
||||
import TemplatesAlertGroupsList from 'containers/TemplatesAlertGroupsList/TemplatesAlertGroupsList';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
|
||||
import styles from './IntegrationTemplate.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface IntegrationTemplateProps {
|
||||
id: AlertReceiveChannel['id'];
|
||||
template: TemplateForEdit;
|
||||
templateBody: string;
|
||||
onHide: () => void;
|
||||
onUpdateTemplates: (values: any) => void;
|
||||
onUpdateRoute: (values: any) => void;
|
||||
}
|
||||
|
||||
const IntegrationTemplate = observer((props: IntegrationTemplateProps) => {
|
||||
const { id, onHide, template, onUpdateTemplates, onUpdateRoute, templateBody } = props;
|
||||
|
||||
const [isCheatSheetVisible, setIsCheatSheetVisible] = useState<boolean>(false);
|
||||
const [chatOps, setChatOps] = useState(undefined);
|
||||
const [alertGroupPayload, setAlertGroupPayload] = useState<JSON>(undefined);
|
||||
const [changedTemplateBody, setChangedTemplateBody] = useState<string>(templateBody);
|
||||
const [resultError, setResultError] = useState<string>(undefined);
|
||||
|
||||
const onShowCheatSheet = useCallback(() => {
|
||||
setIsCheatSheetVisible(true);
|
||||
}, []);
|
||||
|
||||
const onCloseCheatSheet = useCallback(() => {
|
||||
setIsCheatSheetVisible(false);
|
||||
}, []);
|
||||
|
||||
const getChangeHandler = () => {
|
||||
return debounce((value: string) => {
|
||||
setChangedTemplateBody(value);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const onEditPayload = (alertPayload: string) => {
|
||||
if (alertPayload !== null) {
|
||||
try {
|
||||
const jsonPayload = JSON.parse(alertPayload);
|
||||
if (typeof jsonPayload === 'object') {
|
||||
setResultError(undefined);
|
||||
setAlertGroupPayload(JSON.parse(alertPayload));
|
||||
} else {
|
||||
setResultError('Please check your JSON format');
|
||||
}
|
||||
} catch (e) {
|
||||
setResultError(e.message);
|
||||
}
|
||||
} else {
|
||||
setResultError(undefined);
|
||||
setAlertGroupPayload(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectAlertGroup = useCallback((alertGroup: Alert) => {
|
||||
if (template.additionalData?.chatOpsName) {
|
||||
setChatOps({
|
||||
permalink: alertGroup?.permalinks[template.additionalData?.chatOpsName],
|
||||
name: template.additionalData?.chatOpsName,
|
||||
comment: template.additionalData?.data,
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSaveAndFollowLink = useCallback(
|
||||
(link: string) => {
|
||||
onHide();
|
||||
window.open(link, '_blank');
|
||||
},
|
||||
[onUpdateTemplates, onUpdateRoute, changedTemplateBody]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
template.isRoute
|
||||
? onUpdateRoute({ [template.name]: changedTemplateBody })
|
||||
: onUpdateTemplates({ [template.name]: changedTemplateBody });
|
||||
|
||||
onHide();
|
||||
}, [onUpdateTemplates, changedTemplateBody]);
|
||||
|
||||
const getCheatSheet = (templateName) => {
|
||||
switch (templateName) {
|
||||
case 'Grouping':
|
||||
case 'Autoresolve':
|
||||
return groupingTemplateCheatSheet;
|
||||
case 'Web titile':
|
||||
case 'Web message':
|
||||
case 'Web image':
|
||||
return webTitleTemplateCheatSheet;
|
||||
case 'Auto acknowledge':
|
||||
case 'Source link':
|
||||
case 'Phone call':
|
||||
case 'SMS':
|
||||
case 'Slack title':
|
||||
case 'Slack message':
|
||||
case 'Slack image':
|
||||
case 'Telegram title':
|
||||
case 'Telegram message':
|
||||
case 'Telegram image':
|
||||
case 'Email title':
|
||||
case 'Email message':
|
||||
return slackMessageTemplateCheatSheet;
|
||||
default:
|
||||
return webTitleTemplateCheatSheet;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Drawer
|
||||
title={
|
||||
<div className={cx('title-container')}>
|
||||
<HorizontalGroup justify="space-between" align="flex-start">
|
||||
<VerticalGroup>
|
||||
<Text.Title level={3}>Edit {template.displayName} template</Text.Title>
|
||||
{template.description && <Text type="secondary">{template.description}</Text>}
|
||||
</VerticalGroup>
|
||||
|
||||
<HorizontalGroup>
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleSubmit}>
|
||||
Save
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
}
|
||||
onClose={onHide}
|
||||
closeOnMaskClick={false}
|
||||
width={'95%'}
|
||||
>
|
||||
<div className={cx('container')}>
|
||||
<TemplatesAlertGroupsList
|
||||
alertReceiveChannelId={id}
|
||||
onEditPayload={onEditPayload}
|
||||
onSelectAlertGroup={onSelectAlertGroup}
|
||||
/>
|
||||
{isCheatSheetVisible ? (
|
||||
<CheatSheet cheatSheetData={getCheatSheet(template.displayName)} onClose={onCloseCheatSheet} />
|
||||
) : (
|
||||
<>
|
||||
<div className={cx('template-block-codeeditor')}>
|
||||
<div className={cx('template-editor-block-title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>Template editor</Text>
|
||||
|
||||
<Button variant="secondary" fill="outline" onClick={onShowCheatSheet} icon="book" size="sm">
|
||||
Cheatsheat
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
|
||||
<MonacoJinja2Editor
|
||||
value={templateBody}
|
||||
data={undefined}
|
||||
showLineNumbers={true}
|
||||
height={'100vh'}
|
||||
onChange={getChangeHandler()}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* {alertGroupPayload || resultError ? ( */}
|
||||
<Result
|
||||
alertReceiveChannelId={id}
|
||||
templateName={template.name}
|
||||
templateBody={changedTemplateBody}
|
||||
alertGroup={undefined}
|
||||
chatOps={chatOps}
|
||||
payload={alertGroupPayload}
|
||||
error={resultError}
|
||||
onSaveAndFollowLink={onSaveAndFollowLink}
|
||||
/>
|
||||
{/* ) : (
|
||||
<div className={cx('template-block-result')}>
|
||||
<div className={cx('template-block-title')}>
|
||||
<Text>Please select Alert group to see end result</Text>
|
||||
</div>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
interface ResultProps {
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
templateName: string;
|
||||
templateBody: string;
|
||||
alertGroup?: Alert;
|
||||
chatOps?: { permalink: string; name: string; comment?: string };
|
||||
payload?: JSON;
|
||||
error?: string;
|
||||
onSaveAndFollowLink?: (link: string) => void;
|
||||
}
|
||||
|
||||
const Result = (props: ResultProps) => {
|
||||
const { alertReceiveChannelId, templateName, chatOps, payload, templateBody, error, onSaveAndFollowLink } = props;
|
||||
|
||||
return (
|
||||
<div className={cx('template-block-result')}>
|
||||
<div className={cx('template-block-title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>Result</Text>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('result')}>
|
||||
{payload || error ? (
|
||||
<VerticalGroup spacing="lg">
|
||||
{error ? (
|
||||
<Block bordered fullWidth withBackground>
|
||||
<Text>{error}</Text>
|
||||
</Block>
|
||||
) : (
|
||||
<Block bordered fullWidth className={cx('block-style')}>
|
||||
<TemplatePreview
|
||||
key={templateName}
|
||||
templateName={templateName}
|
||||
templateBody={templateBody}
|
||||
alertReceiveChannelId={alertReceiveChannelId}
|
||||
payload={payload}
|
||||
/>
|
||||
</Block>
|
||||
)}
|
||||
|
||||
{chatOps && (
|
||||
<VerticalGroup>
|
||||
<Button onClick={() => onSaveAndFollowLink(chatOps.permalink)}>
|
||||
Save and open Alert Group in {chatOps.name}
|
||||
</Button>
|
||||
|
||||
{chatOps.comment && (
|
||||
<Text type="secondary">
|
||||
Click "Acknowledge" and then "Unacknowledge" in Slack to trigger re-rendering.
|
||||
</Text>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
) : (
|
||||
<div>
|
||||
<Block bordered fullWidth className={cx('block-style')}>
|
||||
<Text>You do not have any input data to render result. Please select Alert group to see end result</Text>
|
||||
</Block>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntegrationTemplate;
|
||||
|
|
@ -282,7 +282,7 @@ function QRLoading() {
|
|||
<Text type="primary" className={cx('qr-loader__text')}>
|
||||
Regenerating QR code...
|
||||
</Text>
|
||||
<LoadingPlaceholder />
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,17 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DisconnectButton it renders properly 1`] = `
|
||||
<div>
|
||||
<button
|
||||
class="css-mk7eo3-button disconnect-button"
|
||||
data-testid="test__disconnect"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Disconnect
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DownloadIcons it renders properly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="css-1j7sh2x-vertical-group"
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
<div
|
||||
class="css-ztyofd-layoutChildrenWrapper"
|
||||
>
|
||||
<span
|
||||
class="root text text--primary text--medium text--strong"
|
||||
>
|
||||
Download
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-ztyofd-layoutChildrenWrapper"
|
||||
>
|
||||
<span
|
||||
class="root text text--primary text--medium"
|
||||
>
|
||||
The Grafana IRM app is available on both the App Store and Google Play Store.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-ztyofd-layoutChildrenWrapper"
|
||||
>
|
||||
<div
|
||||
class="css-1j7sh2x-vertical-group"
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
<div
|
||||
class="css-bxa289-layoutChildrenWrapper"
|
||||
>
|
||||
<a
|
||||
href="https://apps.apple.com/us/app/grafana-oncall-preview/id1669759048"
|
||||
rel="noreferrer"
|
||||
style="width: 100%;"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="root icon-block root_bordered root--fullWidth root--withBackground root--hover"
|
||||
>
|
||||
<img
|
||||
alt="Apple"
|
||||
class="icon"
|
||||
src="[object Object]"
|
||||
/>
|
||||
<span
|
||||
class="root text icon-text text--primary text--medium"
|
||||
>
|
||||
iOS
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="css-bxa289-layoutChildrenWrapper"
|
||||
>
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=com.grafana.oncall.prod"
|
||||
rel="noreferrer"
|
||||
style="width: 100%;"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="root icon-block root_bordered root--fullWidth root--hover"
|
||||
>
|
||||
<img
|
||||
alt="Play Store"
|
||||
class="icon"
|
||||
src="[object Object]"
|
||||
/>
|
||||
<span
|
||||
class="root text icon-text text--primary text--medium"
|
||||
>
|
||||
Android
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -153,7 +153,7 @@ const PersonalNotificationSettings = observer((props: PersonalNotificationSettin
|
|||
store={store}
|
||||
/>
|
||||
))}
|
||||
<Timeline.Item number={notificationPolicies.length + 1} color={getColor(notificationPolicies.length)}>
|
||||
<Timeline.Item number={notificationPolicies.length + 1} backgroundColor={getColor(notificationPolicies.length)}>
|
||||
<div className={cx('step')}>
|
||||
<WithPermissionControlTooltip userAction={userAction}>
|
||||
<Button icon="plus" variant="secondary" fill="text" onClick={getAddNotificationPolicyHandler()}>
|
||||
|
|
|
|||
|
|
@ -1,449 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PluginConfigPage If onCallApiUrl is not set in the plugin's meta jsonData, and ONCALL_API_URL is passed in process.env, and there is an error calling selfHostedInstallPlugin, it sets an error message 1`] = `
|
||||
<div>
|
||||
<legend
|
||||
class="css-xim8hk"
|
||||
>
|
||||
Configure Grafana OnCall
|
||||
</legend>
|
||||
<p>
|
||||
This page will help you configure the OnCall plugin 👋
|
||||
</p>
|
||||
<pre
|
||||
data-testid="status-message-block"
|
||||
>
|
||||
<span
|
||||
class="root text text--undefined text--medium"
|
||||
>
|
||||
ohhh nooo an error msg from self hosted install plugin
|
||||
</span>
|
||||
</pre>
|
||||
<button
|
||||
class="css-mk7eo3-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginConfigPage If onCallApiUrl is not set in the plugin's meta jsonData, or in process.env, checkIfPluginIsConnected is not called, and the configuration form is shown 1`] = `
|
||||
<div>
|
||||
<legend
|
||||
class="css-xim8hk"
|
||||
>
|
||||
Configure Grafana OnCall
|
||||
</legend>
|
||||
<p>
|
||||
This page will help you configure the OnCall plugin 👋
|
||||
</p>
|
||||
<form
|
||||
class="css-xs0vux"
|
||||
data-testid="plugin-configuration-form"
|
||||
>
|
||||
<div
|
||||
class="info-block"
|
||||
>
|
||||
<p>
|
||||
1. Launch the OnCall backend
|
||||
</p>
|
||||
<span
|
||||
class="root text text--secondary text--medium"
|
||||
>
|
||||
Run hobby, dev or production backend. See
|
||||
|
||||
<a
|
||||
href="https://github.com/grafana/oncall#getting-started"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
class="root text text--link text--medium"
|
||||
>
|
||||
here
|
||||
</span>
|
||||
</a>
|
||||
|
||||
on how to get started.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="info-block"
|
||||
>
|
||||
<p>
|
||||
2. Let us know the base URL of your OnCall API
|
||||
</p>
|
||||
<span
|
||||
class="root text text--secondary text--medium"
|
||||
>
|
||||
The OnCall backend must be reachable from your Grafana installation. Some examples are:
|
||||
<br />
|
||||
- http://host.docker.internal:8080
|
||||
<br />
|
||||
- http://localhost:8080
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-8e5b3"
|
||||
>
|
||||
<div
|
||||
class="css-jt4xma-Label"
|
||||
>
|
||||
<label>
|
||||
<div
|
||||
class="css-xhqy0o"
|
||||
>
|
||||
OnCall backend URL
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="css-xcstkt-input-wrapper"
|
||||
data-testid="input-wrapper"
|
||||
>
|
||||
<div
|
||||
class="css-1w5c5dq-input-inputWrapper"
|
||||
>
|
||||
<input
|
||||
class="css-1wdli31-input-input"
|
||||
data-testid="onCallApiUrl"
|
||||
name="onCallApiUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="css-1sara2j-button"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Connect
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginConfigPage If onCallApiUrl is set, and checkIfPluginIsConnected returns an error, it sets an error message 1`] = `
|
||||
<div>
|
||||
<legend
|
||||
class="css-xim8hk"
|
||||
>
|
||||
Configure Grafana OnCall
|
||||
</legend>
|
||||
<p>
|
||||
This page will help you configure the OnCall plugin 👋
|
||||
</p>
|
||||
<pre
|
||||
data-testid="status-message-block"
|
||||
>
|
||||
<span
|
||||
class="root text text--undefined text--medium"
|
||||
>
|
||||
ohhh nooo a plugin connection error
|
||||
</span>
|
||||
</pre>
|
||||
<button
|
||||
class="css-mk7eo3-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginConfigPage It doesn't make any network calls if the plugin configured query params are provided 1`] = `
|
||||
<div>
|
||||
<legend
|
||||
class="css-xim8hk"
|
||||
>
|
||||
Configure Grafana OnCall
|
||||
</legend>
|
||||
<p>
|
||||
Plugin is connected! Continue to Grafana OnCall by clicking the
|
||||
|
||||
<img
|
||||
alt="Grafana OnCall Logo"
|
||||
src="[object Object]"
|
||||
width="18"
|
||||
/>
|
||||
icon over there 👈
|
||||
</p>
|
||||
<pre
|
||||
data-testid="status-message-block"
|
||||
>
|
||||
<span
|
||||
class="root text text--undefined text--medium"
|
||||
>
|
||||
Connected to OnCall (v1.2.3, OpenSource)
|
||||
</span>
|
||||
</pre>
|
||||
<button
|
||||
class="css-mk7eo3-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginConfigPage OnCallApiUrl is set, and syncDataWithOnCall does not return an error. It displays properly the plugin connected items based on the license - License: OpenSource 1`] = `
|
||||
<div>
|
||||
<legend
|
||||
class="css-xim8hk"
|
||||
>
|
||||
Configure Grafana OnCall
|
||||
</legend>
|
||||
<p>
|
||||
Plugin is connected! Continue to Grafana OnCall by clicking the
|
||||
|
||||
<img
|
||||
alt="Grafana OnCall Logo"
|
||||
src="[object Object]"
|
||||
width="18"
|
||||
/>
|
||||
icon over there 👈
|
||||
</p>
|
||||
<pre
|
||||
data-testid="status-message-block"
|
||||
>
|
||||
<span
|
||||
class="root text text--undefined text--medium"
|
||||
>
|
||||
Connected to OnCall (v1.2.3, OpenSource)
|
||||
</span>
|
||||
</pre>
|
||||
<button
|
||||
class="css-mk7eo3-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginConfigPage OnCallApiUrl is set, and syncDataWithOnCall does not return an error. It displays properly the plugin connected items based on the license - License: some-other-license 1`] = `
|
||||
<div>
|
||||
<legend
|
||||
class="css-xim8hk"
|
||||
>
|
||||
Configure Grafana OnCall
|
||||
</legend>
|
||||
<p>
|
||||
Plugin is connected! Continue to Grafana OnCall by clicking the
|
||||
|
||||
<img
|
||||
alt="Grafana OnCall Logo"
|
||||
src="[object Object]"
|
||||
width="18"
|
||||
/>
|
||||
icon over there 👈
|
||||
</p>
|
||||
<pre
|
||||
data-testid="status-message-block"
|
||||
>
|
||||
<span
|
||||
class="root text text--undefined text--medium"
|
||||
>
|
||||
Connected to OnCall (v1.2.3, some-other-license)
|
||||
</span>
|
||||
</pre>
|
||||
<div
|
||||
class="css-jt4xma-Label"
|
||||
>
|
||||
<label>
|
||||
<div
|
||||
class="css-xhqy0o"
|
||||
>
|
||||
This is a cloud managed configuration.
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginConfigPage OnCallApiUrl is set, and syncDataWithOnCall returns an error 1`] = `
|
||||
<div>
|
||||
<legend
|
||||
class="css-xim8hk"
|
||||
>
|
||||
Configure Grafana OnCall
|
||||
</legend>
|
||||
<p>
|
||||
This page will help you configure the OnCall plugin 👋
|
||||
</p>
|
||||
<pre
|
||||
data-testid="status-message-block"
|
||||
>
|
||||
<span
|
||||
class="root text text--undefined text--medium"
|
||||
>
|
||||
ohhh noooo a sync issue
|
||||
</span>
|
||||
</pre>
|
||||
<button
|
||||
class="css-1sara2j-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Retry Sync
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginConfigPage Plugin reset: successful - false 1`] = `
|
||||
<div>
|
||||
<legend
|
||||
class="css-xim8hk"
|
||||
>
|
||||
Configure Grafana OnCall
|
||||
</legend>
|
||||
<p>
|
||||
This page will help you configure the OnCall plugin 👋
|
||||
</p>
|
||||
<pre
|
||||
data-testid="status-message-block"
|
||||
>
|
||||
<span
|
||||
class="root text text--undefined text--medium"
|
||||
>
|
||||
There was an error resetting your plugin, try again.
|
||||
</span>
|
||||
</pre>
|
||||
<button
|
||||
class="css-mk7eo3-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginConfigPage Plugin reset: successful - true 1`] = `
|
||||
<div>
|
||||
<legend
|
||||
class="css-xim8hk"
|
||||
>
|
||||
Configure Grafana OnCall
|
||||
</legend>
|
||||
<p>
|
||||
This page will help you configure the OnCall plugin 👋
|
||||
</p>
|
||||
<form
|
||||
class="css-xs0vux"
|
||||
data-testid="plugin-configuration-form"
|
||||
>
|
||||
<div
|
||||
class="info-block"
|
||||
>
|
||||
<p>
|
||||
1. Launch the OnCall backend
|
||||
</p>
|
||||
<span
|
||||
class="root text text--secondary text--medium"
|
||||
>
|
||||
Run hobby, dev or production backend. See
|
||||
|
||||
<a
|
||||
href="https://github.com/grafana/oncall#getting-started"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
class="root text text--link text--medium"
|
||||
>
|
||||
here
|
||||
</span>
|
||||
</a>
|
||||
|
||||
on how to get started.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="info-block"
|
||||
>
|
||||
<p>
|
||||
2. Let us know the base URL of your OnCall API
|
||||
</p>
|
||||
<span
|
||||
class="root text text--secondary text--medium"
|
||||
>
|
||||
The OnCall backend must be reachable from your Grafana installation. Some examples are:
|
||||
<br />
|
||||
- http://host.docker.internal:8080
|
||||
<br />
|
||||
- http://localhost:8080
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-8e5b3"
|
||||
>
|
||||
<div
|
||||
class="css-jt4xma-Label"
|
||||
>
|
||||
<label>
|
||||
<div
|
||||
class="css-xhqy0o"
|
||||
>
|
||||
OnCall backend URL
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="css-xcstkt-input-wrapper"
|
||||
data-testid="input-wrapper"
|
||||
>
|
||||
<div
|
||||
class="css-1w5c5dq-input-inputWrapper"
|
||||
>
|
||||
<input
|
||||
class="css-1wdli31-input-input"
|
||||
data-testid="onCallApiUrl"
|
||||
name="onCallApiUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="css-1sara2j-button"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Connect
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,278 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConfigurationForm It doesn't allow the user to submit if the URL is invalid 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<form
|
||||
class="css-xs0vux"
|
||||
data-testid="plugin-configuration-form"
|
||||
>
|
||||
<div
|
||||
class="info-block"
|
||||
>
|
||||
<p>
|
||||
1. Launch the OnCall backend
|
||||
</p>
|
||||
<span
|
||||
class="root text text--secondary text--medium"
|
||||
>
|
||||
Run hobby, dev or production backend. See
|
||||
|
||||
<a
|
||||
href="https://github.com/grafana/oncall#getting-started"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
class="root text text--link text--medium"
|
||||
>
|
||||
here
|
||||
</span>
|
||||
</a>
|
||||
|
||||
on how to get started.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="info-block"
|
||||
>
|
||||
<p>
|
||||
2. Let us know the base URL of your OnCall API
|
||||
</p>
|
||||
<span
|
||||
class="root text text--secondary text--medium"
|
||||
>
|
||||
The OnCall backend must be reachable from your Grafana installation. Some examples are:
|
||||
<br />
|
||||
- http://host.docker.internal:8080
|
||||
<br />
|
||||
- http://localhost:8080
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-8e5b3"
|
||||
>
|
||||
<div
|
||||
class="css-jt4xma-Label"
|
||||
>
|
||||
<label>
|
||||
<div
|
||||
class="css-xhqy0o"
|
||||
>
|
||||
OnCall backend URL
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="css-1wz1ggz-input-wrapper"
|
||||
data-testid="input-wrapper"
|
||||
>
|
||||
<div
|
||||
class="css-1w5c5dq-input-inputWrapper"
|
||||
>
|
||||
<input
|
||||
class="css-uzu9xn-input-input"
|
||||
data-testid="onCallApiUrl"
|
||||
name="onCallApiUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-1gd2lua"
|
||||
>
|
||||
<div
|
||||
class="css-mw0th3"
|
||||
role="alert"
|
||||
>
|
||||
<div
|
||||
class="css-wf08df-Icon"
|
||||
>
|
||||
<svg
|
||||
class="css-1ah41zt"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12,16a1,1,0,1,0,1,1A1,1,0,0,0,12,16Zm10.67,1.47-8.05-14a3,3,0,0,0-5.24,0l-8,14A3,3,0,0,0,3.94,22H20.06a3,3,0,0,0,2.61-4.53Zm-1.73,2a1,1,0,0,1-.88.51H3.94a1,1,0,0,1-.88-.51,1,1,0,0,1,0-1l8-14a1,1,0,0,1,1.78,0l8.05,14A1,1,0,0,1,20.94,19.49ZM12,8a1,1,0,0,0-1,1v4a1,1,0,0,0,2,0V9A1,1,0,0,0,12,8Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
Must be a valid URL
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="css-1sara2j-button"
|
||||
disabled=""
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Connect
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`ConfigurationForm It shows an error message if the self hosted plugin API call fails 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<form
|
||||
class="css-xs0vux"
|
||||
data-testid="plugin-configuration-form"
|
||||
>
|
||||
<div
|
||||
class="info-block"
|
||||
>
|
||||
<p>
|
||||
1. Launch the OnCall backend
|
||||
</p>
|
||||
<span
|
||||
class="root text text--secondary text--medium"
|
||||
>
|
||||
Run hobby, dev or production backend. See
|
||||
|
||||
<a
|
||||
href="https://github.com/grafana/oncall#getting-started"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
class="root text text--link text--medium"
|
||||
>
|
||||
here
|
||||
</span>
|
||||
</a>
|
||||
|
||||
on how to get started.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="info-block"
|
||||
>
|
||||
<p>
|
||||
2. Let us know the base URL of your OnCall API
|
||||
</p>
|
||||
<span
|
||||
class="root text text--secondary text--medium"
|
||||
>
|
||||
The OnCall backend must be reachable from your Grafana installation. Some examples are:
|
||||
<br />
|
||||
- http://host.docker.internal:8080
|
||||
<br />
|
||||
- http://localhost:8080
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="css-8e5b3"
|
||||
>
|
||||
<div
|
||||
class="css-jt4xma-Label"
|
||||
>
|
||||
<label>
|
||||
<div
|
||||
class="css-xhqy0o"
|
||||
>
|
||||
OnCall backend URL
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="css-xcstkt-input-wrapper"
|
||||
data-testid="input-wrapper"
|
||||
>
|
||||
<div
|
||||
class="css-1w5c5dq-input-inputWrapper"
|
||||
>
|
||||
<input
|
||||
class="css-1wdli31-input-input"
|
||||
data-testid="onCallApiUrl"
|
||||
name="onCallApiUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pre>
|
||||
<span
|
||||
class="root text text--link text--medium"
|
||||
>
|
||||
ohhh nooo there was an error from the OnCall API
|
||||
</span>
|
||||
</pre>
|
||||
<div
|
||||
class="root info-block root--withBackground"
|
||||
>
|
||||
<span
|
||||
class="root text text--secondary text--medium"
|
||||
>
|
||||
Need help?
|
||||
<br />
|
||||
- Reach out to the OnCall team in the
|
||||
|
||||
<a
|
||||
href="https://grafana.slack.com/archives/C02LSUUSE2G"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
class="root text text--link text--medium"
|
||||
>
|
||||
#grafana-oncall
|
||||
</span>
|
||||
</a>
|
||||
|
||||
community Slack channel
|
||||
<br />
|
||||
- Ask questions on our GitHub Discussions page
|
||||
|
||||
<a
|
||||
href="https://github.com/grafana/oncall/discussions/categories/q-a"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
class="root text text--link text--medium"
|
||||
>
|
||||
here
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<br />
|
||||
- Or file bugs on our GitHub Issues page
|
||||
|
||||
<a
|
||||
href="https://github.com/grafana/oncall/issues"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span
|
||||
class="root text text--link text--medium"
|
||||
>
|
||||
here
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
class="css-1sara2j-button"
|
||||
type="submit"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Connect
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RemoveCurrentConfigurationButton It renders properly when disabled 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<button
|
||||
class="css-mk7eo3-button"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`RemoveCurrentConfigurationButton It renders properly when enabled 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<button
|
||||
class="css-mk7eo3-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Remove current configuration
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StatusMessageBlock It renders properly 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<pre
|
||||
data-testid="status-message-block"
|
||||
>
|
||||
<span
|
||||
class="root text text--undefined text--medium"
|
||||
>
|
||||
helloooo
|
||||
</span>
|
||||
</pre>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
import { LoadingPlaceholder, Alert as AlertComponent } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
|
|
@ -18,16 +18,20 @@ const cx = cn.bind(styles);
|
|||
interface TemplatePreviewProps {
|
||||
templateName: string;
|
||||
templateBody: string | null;
|
||||
payload?: JSON;
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
onEditClick: () => void;
|
||||
onEditClick?: () => void;
|
||||
alertGroupId?: Alert['pk'];
|
||||
active?: boolean;
|
||||
onResult?: (result) => void;
|
||||
}
|
||||
|
||||
const TemplatePreview = observer((props: TemplatePreviewProps) => {
|
||||
const { templateName, templateBody, alertReceiveChannelId, alertGroupId } = props;
|
||||
const { templateName, templateBody, payload, alertReceiveChannelId, alertGroupId } = props;
|
||||
|
||||
const [result, setResult] = useState<{ preview: string | null } | undefined>(undefined);
|
||||
const [isCondition, setIsCondition] = useState(false);
|
||||
// const [conditionalResult, setConditionalResult] = useState()
|
||||
|
||||
const store = useStore();
|
||||
const { alertReceiveChannelStore, alertGroupStore } = store;
|
||||
|
|
@ -35,9 +39,16 @@ const TemplatePreview = observer((props: TemplatePreviewProps) => {
|
|||
const handleTemplateBodyChange = useDebouncedCallback(() => {
|
||||
(alertGroupId
|
||||
? alertGroupStore.renderPreview(alertGroupId, templateName, templateBody)
|
||||
: alertReceiveChannelStore.renderPreview(alertReceiveChannelId, templateName, templateBody)
|
||||
: alertReceiveChannelStore.renderPreview(alertReceiveChannelId, templateName, templateBody, payload)
|
||||
)
|
||||
.then(setResult)
|
||||
.then((data) => {
|
||||
setResult(data);
|
||||
if (data?.preview === 'True') {
|
||||
setIsCondition(true);
|
||||
} else {
|
||||
setIsCondition(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response?.data?.length > 0) {
|
||||
openErrorNotification(err.response.data);
|
||||
|
|
@ -47,15 +58,33 @@ const TemplatePreview = observer((props: TemplatePreviewProps) => {
|
|||
});
|
||||
}, 1000);
|
||||
|
||||
useEffect(handleTemplateBodyChange, [templateBody]);
|
||||
useEffect(handleTemplateBodyChange, [templateBody, payload]);
|
||||
// onResult(result);
|
||||
|
||||
return result ? (
|
||||
<div
|
||||
className={cx('message')}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(result.preview || ''),
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
{templateName.includes('condition_template') ? (
|
||||
<AlertComponent severity={isCondition ? 'success' : 'error'} title="">
|
||||
{isCondition ? (
|
||||
'True'
|
||||
) : (
|
||||
<div
|
||||
className={cx('message')}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(result.preview || ''),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AlertComponent>
|
||||
) : (
|
||||
<div
|
||||
className={cx('message')}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(result.preview || ''),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
.template-block-title {
|
||||
padding: 16px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.template-block-list {
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.alert-group-payload-view {
|
||||
background-color: var(--primary-background);
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Tooltip, Icon, VerticalGroup, IconButton, Badge } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
import MonacoJinja2Editor from 'components/MonacoJinja2Editor/MonacoJinja2Editor';
|
||||
import SourceCode from 'components/SourceCode/SourceCode';
|
||||
import Text from 'components/Text/Text';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||
import { Alert } from 'models/alertgroup/alertgroup.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './TemplatesAlertGroupsList.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface TemplatesAlertGroupsListProps {
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
onSelectAlertGroup?: (alertGroup: Alert) => void;
|
||||
onEditPayload?: (payload: string) => void;
|
||||
}
|
||||
|
||||
const TemplatesAlertGroupsList = (props: TemplatesAlertGroupsListProps) => {
|
||||
const { alertReceiveChannelId, onEditPayload, onSelectAlertGroup } = props;
|
||||
const store = useStore();
|
||||
const [alertGroupsList, setAlertGroupsList] = useState(undefined);
|
||||
const [selectedAlertPayload, setSelectedAlertPayload] = useState<string>(undefined);
|
||||
const [selectedAlertName, setSelectedAlertName] = useState<string>(undefined);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
store.alertGroupStore
|
||||
.getAlertGroupsForIntegration(alertReceiveChannelId)
|
||||
.then((result) => setAlertGroupsList(result.slice(0, 30)));
|
||||
}, []);
|
||||
|
||||
const getChangeHandler = () => {
|
||||
return debounce((value: string) => {
|
||||
onEditPayload(value);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const returnToListView = () => {
|
||||
setIsEditMode(false);
|
||||
setSelectedAlertPayload(undefined);
|
||||
onEditPayload(null);
|
||||
};
|
||||
|
||||
const getAlertGroupPayload = async (id) => {
|
||||
const groupedAlert = await store.alertGroupStore.getAlertsFromGroup(id);
|
||||
const currentIncidentRawResponse = await store.alertGroupStore.getPayloadForIncident(groupedAlert?.alerts[0]?.id);
|
||||
setSelectedAlertName(getAlertGroupName(groupedAlert));
|
||||
setSelectedAlertPayload(currentIncidentRawResponse?.raw_request_data);
|
||||
onSelectAlertGroup(groupedAlert);
|
||||
onEditPayload(JSON.stringify(currentIncidentRawResponse?.raw_request_data));
|
||||
};
|
||||
|
||||
const getAlertGroupName = (alertGroup: Alert) => {
|
||||
return alertGroup.inside_organization_number
|
||||
? `#${alertGroup.inside_organization_number} ${alertGroup.render_for_web.title}`
|
||||
: alertGroup.render_for_web.title;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx('template-block-list')}>
|
||||
{selectedAlertPayload ? (
|
||||
<>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<div className={cx('template-block-title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>Edit {selectedAlertName}</Text>
|
||||
|
||||
<HorizontalGroup>
|
||||
<IconButton name="times" onClick={() => returnToListView()} />
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-list')}>
|
||||
<MonacoJinja2Editor
|
||||
value={JSON.stringify(selectedAlertPayload, null, 4)}
|
||||
data={undefined}
|
||||
height={'100vh'}
|
||||
onChange={getChangeHandler()}
|
||||
showLineNumbers
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={cx('template-block-title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>{selectedAlertName}</Text>
|
||||
|
||||
<HorizontalGroup>
|
||||
<IconButton name="edit" onClick={() => setIsEditMode(true)} />
|
||||
<IconButton name="times" onClick={() => returnToListView()} />
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-list')}>
|
||||
<VerticalGroup>
|
||||
<Badge style={{ margin: '16px' }} color="blue" text="Last alert payload" />
|
||||
<SourceCode className={cx('alert-group-payload-view')} noMaxHeight>
|
||||
{JSON.stringify(selectedAlertPayload, null, 4)}
|
||||
</SourceCode>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<div className={cx('template-block-title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<Text>Edit custom payload</Text>
|
||||
|
||||
<HorizontalGroup>
|
||||
<IconButton name="times" onClick={() => returnToListView()} />
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-list')}>
|
||||
<MonacoJinja2Editor
|
||||
value={null}
|
||||
data={undefined}
|
||||
height={'100vh'}
|
||||
onChange={getChangeHandler()}
|
||||
showLineNumbers
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={cx('template-block-title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup>
|
||||
<Text>Recent Alert groups</Text>
|
||||
<Tooltip content="Here will be information about alert groups">
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
|
||||
<Button variant="secondary" fill="outline" onClick={() => setIsEditMode(true)} size="sm">
|
||||
Use custom payload
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('alert-groups-list')}>
|
||||
{alertGroupsList?.length > 0 ? (
|
||||
<>
|
||||
{alertGroupsList.map((alertGroup) => {
|
||||
return (
|
||||
<div key={alertGroup.pk}>
|
||||
<Button fill="text" onClick={() => getAlertGroupPayload(alertGroup.pk)}>
|
||||
{getAlertGroupName(alertGroup)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<Badge
|
||||
color="blue"
|
||||
text={
|
||||
<HorizontalGroup>
|
||||
<Icon name="info-circle" />
|
||||
<Text>
|
||||
This integration did not receive any alerts. Use custom payload example to preview results.
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplatesAlertGroupsList;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import React, { useEffect } from 'react';
|
||||
|
||||
import { HorizontalGroup } from '@grafana/ui';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import Text from 'components/Text/Text';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
interface UserDisplayProps {
|
||||
id: User['pk'];
|
||||
}
|
||||
|
||||
const UserDisplayWithAvatar = observer(({ id }: UserDisplayProps) => {
|
||||
const { userStore } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!userStore.items[id]) {
|
||||
userStore.updateItem(id);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const user = userStore.items[id];
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Avatar size="small" src={user.avatar}></Avatar>
|
||||
<Text type="secondary">{user.email}</Text>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
});
|
||||
|
||||
export default UserDisplayWithAvatar;
|
||||
|
|
@ -10,7 +10,7 @@ export enum MaintenanceMode {
|
|||
|
||||
export interface AlertReceiveChannel {
|
||||
id: string;
|
||||
integration: number;
|
||||
integration: string;
|
||||
smile_code: string;
|
||||
verbal_name: string;
|
||||
author: User['pk'];
|
||||
|
|
@ -23,6 +23,7 @@ export interface AlertReceiveChannel {
|
|||
instructions: string;
|
||||
demo_alert_enabled: boolean;
|
||||
maintenance_mode?: MaintenanceMode;
|
||||
maintenance_till?: string;
|
||||
heartbeat: Heartbeat | null;
|
||||
is_available_for_integration_heartbeat: boolean;
|
||||
}
|
||||
|
|
@ -31,5 +32,3 @@ export interface AlertReceiveChannelChoice {
|
|||
display_name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const MaintenanceIntegration = 24;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
|
||||
export class AlertReceiveChannelStore extends BaseStore {
|
||||
@observable.shallow
|
||||
// searchResult: { count?: number; results?: Array<AlertReceiveChannel['id']> } = {};
|
||||
searchResult: Array<AlertReceiveChannel['id']>;
|
||||
|
||||
@observable.shallow
|
||||
|
|
@ -66,6 +67,15 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
return this.searchResult.map(
|
||||
(alertReceiveChannelId: AlertReceiveChannel['id']) => this.items?.[alertReceiveChannelId]
|
||||
);
|
||||
|
||||
// return {
|
||||
// count: this.searchResult.count,
|
||||
// results:
|
||||
// this.searchResult.results &&
|
||||
// this.searchResult.results.map(
|
||||
// (alertReceiveChannelId: AlertReceiveChannel['id']) => this.items?.[alertReceiveChannelId]
|
||||
// ),
|
||||
// };
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
@ -82,6 +92,27 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
|
||||
@action
|
||||
async updateItems(query: any = '') {
|
||||
// const filters = typeof query === 'string' ? { search: query } : query;
|
||||
// const { search } = filters;
|
||||
// const { count, results } = await makeRequest(this.path, { params: { search, page } });
|
||||
|
||||
// this.items = {
|
||||
// ...this.items,
|
||||
// ...results.reduce(
|
||||
// (acc: { [key: number]: AlertReceiveChannel }, item: AlertReceiveChannel) => ({
|
||||
// ...acc,
|
||||
// [item.id]: omit(item, 'heartbeat'),
|
||||
// }),
|
||||
// {}
|
||||
// ),
|
||||
// };
|
||||
|
||||
// this.searchResult = result.map((item: AlertReceiveChannel) => item.id);
|
||||
// this.searchResult = {
|
||||
// count,
|
||||
// results: results.map((item: AlertReceiveChannel) => item.id),
|
||||
// };
|
||||
|
||||
const params = typeof query === 'string' ? { search: query } : query;
|
||||
|
||||
const result = await makeRequest(this.path, { params });
|
||||
|
|
@ -131,7 +162,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
}
|
||||
|
||||
@action
|
||||
async updateChannelFilters(alertReceiveChannelId: AlertReceiveChannel['id']) {
|
||||
async updateChannelFilters(alertReceiveChannelId: AlertReceiveChannel['id'], isOverwrite = false) {
|
||||
const response = await makeRequest(`/channel_filters/`, {
|
||||
params: { alert_receive_channel: alertReceiveChannelId },
|
||||
});
|
||||
|
|
@ -149,6 +180,13 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
...channelFilters,
|
||||
};
|
||||
|
||||
if (isOverwrite) {
|
||||
// This is needed because on Move Up/Down/Removal the store no longer reflects correct state
|
||||
this.channelFilters = {
|
||||
...channelFilters,
|
||||
};
|
||||
}
|
||||
|
||||
this.channelFilterIds = {
|
||||
...this.channelFilterIds,
|
||||
[alertReceiveChannelId]: response.map((channelFilter: ChannelFilter) => channelFilter.id),
|
||||
|
|
@ -206,7 +244,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
|
||||
await makeRequest(`/channel_filters/${channelFilterId}/move_to_position/?position=${newIndex}`, { method: 'PUT' });
|
||||
|
||||
this.updateChannelFilters(alertReceiveChannelId);
|
||||
this.updateChannelFilters(alertReceiveChannelId, true);
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
@ -224,7 +262,7 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
method: 'DELETE',
|
||||
});
|
||||
|
||||
this.updateChannelFilters(channelFilter.alert_receive_channel);
|
||||
this.updateChannelFilters(channelFilter.alert_receive_channel, true);
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
@ -341,10 +379,10 @@ export class AlertReceiveChannelStore extends BaseStore {
|
|||
await makeRequest(`/channel_filters/${id}/send_demo_alert/`, { method: 'POST' }).catch(showApiError);
|
||||
}
|
||||
|
||||
async renderPreview(id: AlertReceiveChannel['id'], template_name: string, template_body: string) {
|
||||
async renderPreview(id: AlertReceiveChannel['id'], template_name: string, template_body: string, payload: JSON) {
|
||||
return await makeRequest(`${this.path}${id}/preview_template/`, {
|
||||
method: 'POST',
|
||||
data: { template_name, template_body },
|
||||
data: { template_name, template_body, payload },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export enum MaintenanceMode {
|
|||
|
||||
export interface AlertReceiveChannel {
|
||||
id: string;
|
||||
integration: number;
|
||||
integration: any;
|
||||
smile_code: string;
|
||||
verbal_name: string;
|
||||
description: string;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,35 @@
|
|||
export interface AlertTemplatesDTO {
|
||||
slack_title_template: string;
|
||||
slack_title_template_is_default: boolean;
|
||||
web_title_template: string;
|
||||
web_title_template_is_default: boolean;
|
||||
sms_title_template: string;
|
||||
sms_title_template_is_default: boolean;
|
||||
phone_call_title_template: string;
|
||||
phone_call_title_template_is_default: boolean;
|
||||
email_title_template: string;
|
||||
email_title_template_is_default: boolean;
|
||||
telegram_title_template: string;
|
||||
telegram_title_template_is_default: boolean;
|
||||
slack_message_template: string;
|
||||
slack_message_template_is_default: boolean;
|
||||
web_message_template: string;
|
||||
web_message_template_is_default: boolean;
|
||||
email_message_template: string;
|
||||
email_message_template_is_default: boolean;
|
||||
telegram_message_template: string;
|
||||
telegram_message_template_is_default: boolean;
|
||||
slack_image_url_template: string;
|
||||
slack_image_url_template_is_default: boolean;
|
||||
web_image_url_template: string;
|
||||
web_image_url_template_is_default: boolean;
|
||||
telegram_image_url_template: string;
|
||||
telegram_image_url_template_is_default: boolean;
|
||||
grouping_id_template: string;
|
||||
grouping_id_template_is_default: boolean;
|
||||
resolve_condition_template: string;
|
||||
resolve_condition_template_is_default: boolean;
|
||||
acknowledge_condition_template: string;
|
||||
acknowledge_condition_template_is_default: boolean;
|
||||
payload_example: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { action, observable } from 'mobx';
|
||||
import qs from 'query-string';
|
||||
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel';
|
||||
import BaseStore from 'models/base_store';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { makeRequest } from 'network';
|
||||
|
|
@ -131,6 +132,17 @@ export class AlertGroupStore extends BaseStore {
|
|||
return this.searchResult[query].map((id: Alert['pk']) => this.items[id]);
|
||||
}
|
||||
|
||||
async getAlertGroupsForIntegration(integrationId: AlertReceiveChannel['id']) {
|
||||
const { results } = await makeRequest(`${this.path}`, {
|
||||
params: { integration: integrationId },
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
async getAlertsFromGroup(pk: Alert['pk']) {
|
||||
return await makeRequest(`${this.path}${pk}`, {});
|
||||
}
|
||||
|
||||
@action
|
||||
async updateSilenceOptions() {
|
||||
this.silenceOptions = await makeRequest(`${this.path}silence_options/`, {});
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ export interface Alert {
|
|||
is_restricted: boolean;
|
||||
channel: Channel;
|
||||
slack_permalink?: string;
|
||||
permalinks: {
|
||||
slack: string;
|
||||
telegram: string;
|
||||
};
|
||||
declare_incident_link?: string;
|
||||
related_users: User[];
|
||||
render_after_resolve_report_json?: TimeLineItem[];
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import PluginLink from 'components/PluginLink/PluginLink';
|
|||
import Tag from 'components/Tag/Tag';
|
||||
import Text from 'components/Text/Text';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { MaintenanceIntegration } from 'models/alert_receive_channel';
|
||||
import { Alert as AlertType, Alert, IncidentStatus } from 'models/alertgroup/alertgroup.types';
|
||||
import { User } from 'models/user/user.types';
|
||||
import { SilenceButtonCascader } from 'pages/incidents/parts/SilenceButtonCascader';
|
||||
|
|
@ -188,35 +187,31 @@ export function getActionButtons(incident: AlertType, cx: any, callbacks: { [key
|
|||
|
||||
const buttons = [];
|
||||
|
||||
if (incident.alert_receive_channel.integration !== MaintenanceIntegration) {
|
||||
if (incident.status === IncidentStatus.Silenced) {
|
||||
buttons.push(
|
||||
<WithPermissionControlTooltip key="silence" userAction={UserActions.AlertGroupsWrite}>
|
||||
<Button disabled={incident.loading || incident.is_restricted} variant="secondary" onClick={onUnsilence}>
|
||||
Unsilence
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
);
|
||||
} else if (incident.status !== IncidentStatus.Resolved) {
|
||||
buttons.push(
|
||||
<SilenceButtonCascader
|
||||
className={cx('silence-button-inline')}
|
||||
key="silence"
|
||||
disabled={incident.loading || incident.is_restricted}
|
||||
onSelect={onSilence}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (incident.status === IncidentStatus.Silenced) {
|
||||
buttons.push(
|
||||
<WithPermissionControlTooltip key="silence" userAction={UserActions.AlertGroupsWrite}>
|
||||
<Button disabled={incident.loading || incident.is_restricted} variant="secondary" onClick={onUnsilence}>
|
||||
Unsilence
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
);
|
||||
} else if (incident.status !== IncidentStatus.Resolved) {
|
||||
buttons.push(
|
||||
<SilenceButtonCascader
|
||||
className={cx('silence-button-inline')}
|
||||
key="silence"
|
||||
disabled={incident.loading || incident.is_restricted}
|
||||
onSelect={onSilence}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!incident.resolved && !incident.acknowledged) {
|
||||
buttons.push(acknowledgeButton, resolveButton);
|
||||
} else if (!incident.resolved) {
|
||||
buttons.push(unacknowledgeButton, resolveButton);
|
||||
} else {
|
||||
buttons.push(unresolveButton);
|
||||
}
|
||||
if (!incident.resolved && !incident.acknowledged) {
|
||||
buttons.push(acknowledgeButton, resolveButton);
|
||||
} else if (!incident.resolved) {
|
||||
buttons.push(resolveButton);
|
||||
buttons.push(unacknowledgeButton, resolveButton);
|
||||
} else {
|
||||
buttons.push(unresolveButton);
|
||||
}
|
||||
|
||||
return <HorizontalGroup justify="flex-end">{buttons}</HorizontalGroup>;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import Text from 'components/Text/Text';
|
|||
import AttachIncidentForm from 'containers/AttachIncidentForm/AttachIncidentForm';
|
||||
import EscalationVariants from 'containers/EscalationVariants/EscalationVariants';
|
||||
import { prepareForEdit, prepareForUpdate } from 'containers/EscalationVariants/EscalationVariants.helpers';
|
||||
import IntegrationSettings from 'containers/IntegrationSettings/IntegrationSettings';
|
||||
// import IntegrationSettings from 'containers/IntegrationSettings/IntegrationSettings';
|
||||
import { IntegrationSettingsTab } from 'containers/IntegrationSettings/IntegrationSettings.types';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import {
|
||||
|
|
@ -122,7 +122,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
|
||||
const { errorData, showIntegrationSettings, showAttachIncidentForm } = this.state;
|
||||
const { isNotFoundError, isWrongTeamError } = errorData;
|
||||
const { alertReceiveChannelStore } = store;
|
||||
// const { alertReceiveChannelStore } = store;
|
||||
const { alerts } = store.alertGroupStore;
|
||||
|
||||
const incident = alerts.get(id);
|
||||
|
|
@ -176,22 +176,45 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
</div>
|
||||
</div>
|
||||
{showIntegrationSettings && (
|
||||
<IntegrationSettings
|
||||
alertGroupId={incident.pk}
|
||||
onUpdate={() => {
|
||||
alertReceiveChannelStore.updateItem(incident.alert_receive_channel.id);
|
||||
}}
|
||||
onUpdateTemplates={() => {
|
||||
store.alertGroupStore.getAlert(id);
|
||||
}}
|
||||
startTab={IntegrationSettingsTab.Templates}
|
||||
id={incident.alert_receive_channel.id}
|
||||
onHide={() =>
|
||||
// <IntegrationSettings
|
||||
// alertGroupId={incident.pk}
|
||||
// onUpdate={() => {
|
||||
// alertReceiveChannelStore.updateItem(incident.alert_receive_channel.id);
|
||||
// }}
|
||||
// onUpdateTemplates={() => {
|
||||
// store.alertGroupStore.getAlert(id);
|
||||
// }}
|
||||
// startTab={IntegrationSettingsTab.Templates}
|
||||
// id={incident.alert_receive_channel.id}
|
||||
// onHide={() =>
|
||||
// this.setState({
|
||||
// showIntegrationSettings: undefined,
|
||||
// })
|
||||
// }
|
||||
// />
|
||||
<Modal
|
||||
isOpen
|
||||
title="Edit template"
|
||||
onDismiss={() =>
|
||||
this.setState({
|
||||
showIntegrationSettings: undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Text>
|
||||
Please go to{' '}
|
||||
<PluginLink
|
||||
query={{
|
||||
page: 'integrations',
|
||||
id: incident.alert_receive_channel.id,
|
||||
tab: IntegrationSettingsTab.Templates,
|
||||
}}
|
||||
>
|
||||
Integrations
|
||||
</PluginLink>{' '}
|
||||
to edit this template
|
||||
</Text>
|
||||
</Modal>
|
||||
)}
|
||||
{showAttachIncidentForm && (
|
||||
<AttachIncidentForm
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const getIncidentTagColor = (alert: Alert) => {
|
|||
return getVar('--tag-secondary');
|
||||
};
|
||||
|
||||
function ListMenu({ alert, openMenu }: { alert: Alert; openMenu: React.MouseEventHandler<HTMLElement> }) {
|
||||
function IncidentStatusTag({ alert, openMenu }: { alert: Alert; openMenu: React.MouseEventHandler<HTMLElement> }) {
|
||||
const forwardedRef = useRef<HTMLSpanElement>();
|
||||
|
||||
return (
|
||||
|
|
@ -109,7 +109,7 @@ export const IncidentDropdown: FC<{
|
|||
</div>
|
||||
)}
|
||||
>
|
||||
{({ openMenu }) => <ListMenu alert={alert} openMenu={openMenu} />}
|
||||
{({ openMenu }) => <IncidentStatusTag alert={alert} openMenu={openMenu} />}
|
||||
</WithContextMenu>
|
||||
);
|
||||
}
|
||||
|
|
@ -149,7 +149,7 @@ export const IncidentDropdown: FC<{
|
|||
</div>
|
||||
)}
|
||||
>
|
||||
{({ openMenu }) => <ListMenu alert={alert} openMenu={openMenu} />}
|
||||
{({ openMenu }) => <IncidentStatusTag alert={alert} openMenu={openMenu} />}
|
||||
</WithContextMenu>
|
||||
);
|
||||
}
|
||||
|
|
@ -207,7 +207,7 @@ export const IncidentDropdown: FC<{
|
|||
</div>
|
||||
)}
|
||||
>
|
||||
{({ openMenu }) => <ListMenu alert={alert} openMenu={openMenu} />}
|
||||
{({ openMenu }) => <IncidentStatusTag alert={alert} openMenu={openMenu} />}
|
||||
</WithContextMenu>
|
||||
);
|
||||
}
|
||||
|
|
@ -260,7 +260,7 @@ export const IncidentDropdown: FC<{
|
|||
</div>
|
||||
)}
|
||||
>
|
||||
{({ openMenu }) => <ListMenu alert={alert} openMenu={openMenu} />}
|
||||
{({ openMenu }) => <IncidentStatusTag alert={alert} openMenu={openMenu} />}
|
||||
</WithContextMenu>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -50,6 +50,15 @@ export const pages: { [id: string]: PageDefinition } = [
|
|||
text: 'Integrations',
|
||||
action: UserActions.IntegrationsRead,
|
||||
},
|
||||
{
|
||||
icon: 'plug',
|
||||
id: 'integrations_2',
|
||||
text: 'Integrations 2',
|
||||
path: getPath('integrations_2'),
|
||||
hideFromBreadcrumbs: true,
|
||||
hideFromTabs: true,
|
||||
action: UserActions.IntegrationsRead,
|
||||
},
|
||||
{
|
||||
icon: 'list-ul',
|
||||
id: 'escalations',
|
||||
|
|
@ -96,7 +105,6 @@ export const pages: { [id: string]: PageDefinition } = [
|
|||
hideFromTabs: isTopNavbar(),
|
||||
action: UserActions.ChatOpsRead,
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'link',
|
||||
id: 'outgoing_webhooks_2',
|
||||
|
|
@ -180,6 +188,8 @@ export const ROUTES = {
|
|||
'alert-group': ['alert-groups/:id'],
|
||||
users: ['users', 'users/:id'],
|
||||
integrations: ['integrations', 'integrations/:id'],
|
||||
integrations_2: ['integrations_2'],
|
||||
integration_2: ['integrations_2/:id'],
|
||||
escalations: ['escalations', 'escalations/:id'],
|
||||
schedules: ['schedules'],
|
||||
schedule: ['schedules/:id'],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
.spacing {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { ConfirmModal, HorizontalGroup, Icon } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Tag from 'components/Tag/Tag';
|
||||
import Text from 'components/Text/Text';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel';
|
||||
import { ChannelFilter } from 'models/channel_filter';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { getVar } from 'utils/DOM';
|
||||
|
||||
import styles from './CollapsedIntegrationRouteDisplay.module.scss';
|
||||
import { RouteButtonsDisplay } from './ExpandedIntegrationRouteDisplay';
|
||||
import IntegrationHelper from './Integration2.helper';
|
||||
import IntegrationBlock from './IntegrationBlock';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface CollapsedIntegrationRouteDisplayProps {
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
channelFilterId: ChannelFilter['id'];
|
||||
routeIndex: number;
|
||||
}
|
||||
|
||||
const CollapsedIntegrationRouteDisplay: React.FC<CollapsedIntegrationRouteDisplayProps> = observer(
|
||||
({ channelFilterId, alertReceiveChannelId, routeIndex }) => {
|
||||
const { escalationChainStore, alertReceiveChannelStore } = useStore();
|
||||
const [routeIdForDeletion, setRouteIdForDeletion] = useState<ChannelFilter['id']>(undefined);
|
||||
|
||||
const channelFilter = alertReceiveChannelStore.channelFilters[channelFilterId];
|
||||
if (!channelFilter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const escalationChain = escalationChainStore.items[channelFilter.escalation_chain];
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegrationBlock
|
||||
hasCollapsedBorder
|
||||
key={channelFilterId}
|
||||
heading={
|
||||
<HorizontalGroup justify={'space-between'}>
|
||||
<HorizontalGroup spacing={'md'}>
|
||||
<Tag color={getVar('--tag-primary')}>
|
||||
{IntegrationHelper.getRouteConditionWording(alertReceiveChannelStore.channelFilters, routeIndex)}
|
||||
</Tag>
|
||||
{channelFilter.filtering_term && (
|
||||
<Text type="link">{IntegrationHelper.truncateLine(channelFilter.filtering_term)}</Text>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup>
|
||||
<RouteButtonsDisplay
|
||||
alertReceiveChannelId={alertReceiveChannelId}
|
||||
channelFilterId={channelFilterId}
|
||||
routeIndex={routeIndex}
|
||||
setRouteIdForDeletion={() => setRouteIdForDeletion(channelFilterId)}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
}
|
||||
content={
|
||||
<div className={cx('spacing')}>
|
||||
<HorizontalGroup>
|
||||
{channelFilter.slack_channel?.display_name && (
|
||||
<HorizontalGroup>
|
||||
<Text type="secondary">Publish to ChatOps</Text>
|
||||
<Icon name="slack" />
|
||||
<Text type="primary" strong>
|
||||
{channelFilter.slack_channel.display_name}
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
<Icon name="list-ui-alt" />
|
||||
<Text type="secondary">Escalate to</Text>
|
||||
<PluginLink
|
||||
className={cx('hover-button')}
|
||||
target="_blank"
|
||||
query={{ page: 'escalations', id: channelFilter.escalation_chain }}
|
||||
>
|
||||
<Text type="primary" strong>
|
||||
{escalationChain?.name}
|
||||
</Text>
|
||||
</PluginLink>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{routeIdForDeletion && (
|
||||
<ConfirmModal
|
||||
isOpen
|
||||
title="Delete route?"
|
||||
body="Are you sure you want to delete this route?"
|
||||
confirmText="Delete"
|
||||
icon="exclamation-triangle"
|
||||
onConfirm={onRouteDeleteConfirm}
|
||||
onDismiss={() => setRouteIdForDeletion(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
async function onRouteDeleteConfirm() {
|
||||
setRouteIdForDeletion(undefined);
|
||||
await alertReceiveChannelStore.deleteChannelFilter(routeIdForDeletion);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default CollapsedIntegrationRouteDisplay;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.input {
|
||||
&--short {
|
||||
width: 500px;
|
||||
}
|
||||
&--long {
|
||||
width: 700px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
import React, { useReducer } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Button, HorizontalGroup, InlineLabel, VerticalGroup, Icon, Tooltip, ConfirmModal } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import MonacoJinja2Editor from 'components/MonacoJinja2Editor/MonacoJinja2Editor';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Tag from 'components/Tag/Tag';
|
||||
import Text from 'components/Text/Text';
|
||||
import { ChatOpsConnectors } from 'containers/AlertRules/parts';
|
||||
import EscalationChainSteps from 'containers/EscalationChainSteps/EscalationChainSteps';
|
||||
import GSelect from 'containers/GSelect/GSelect';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates';
|
||||
import { ChannelFilter } from 'models/channel_filter/channel_filter.types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { getVar } from 'utils/DOM';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
|
||||
import styles from './ExpandedIntegrationRouteDisplay.module.scss';
|
||||
import { MONACO_INPUT_HEIGHT_SMALL, MONACO_OPTIONS } from './Integration2.config';
|
||||
import IntegrationHelper from './Integration2.helper';
|
||||
import IntegrationBlock from './IntegrationBlock';
|
||||
import IntegrationBlockItem from './IntegrationBlockItem';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface ExpandedIntegrationRouteDisplayProps {
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
channelFilterId: ChannelFilter['id'];
|
||||
routeIndex: number;
|
||||
templates: AlertTemplatesDTO[];
|
||||
openEditTemplateModal: (templateName: string | string[]) => void;
|
||||
}
|
||||
|
||||
interface ExpandedIntegrationRouteDisplayState {
|
||||
isEscalationCollapsed: boolean;
|
||||
isRefreshingEscalationChains: boolean;
|
||||
routeIdForDeletion: string;
|
||||
}
|
||||
|
||||
const ExpandedIntegrationRouteDisplay: React.FC<ExpandedIntegrationRouteDisplayProps> = observer(
|
||||
({ alertReceiveChannelId, channelFilterId, templates, routeIndex, openEditTemplateModal }) => {
|
||||
const { escalationPolicyStore, escalationChainStore, alertReceiveChannelStore, grafanaTeamStore } = useStore();
|
||||
const hasChatOpsConnectors = false;
|
||||
|
||||
const [{ isEscalationCollapsed, isRefreshingEscalationChains, routeIdForDeletion }, setState] = useReducer(
|
||||
(state: ExpandedIntegrationRouteDisplayState, newState: Partial<ExpandedIntegrationRouteDisplayState>) => ({
|
||||
...state,
|
||||
...newState,
|
||||
}),
|
||||
{
|
||||
isEscalationCollapsed: true,
|
||||
isRefreshingEscalationChains: false,
|
||||
routeIdForDeletion: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const channelFilter = alertReceiveChannelStore.channelFilters[channelFilterId];
|
||||
const channelFiltersTotal = Object.keys(alertReceiveChannelStore.channelFilters);
|
||||
if (!channelFilter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegrationBlock
|
||||
hasCollapsedBorder
|
||||
key={channelFilterId}
|
||||
heading={
|
||||
<HorizontalGroup justify={'space-between'}>
|
||||
<HorizontalGroup spacing={'md'}>
|
||||
<Tag color={getVar('--tag-primary')}>
|
||||
{IntegrationHelper.getRouteConditionWording(alertReceiveChannelStore.channelFilters, routeIndex)}
|
||||
</Tag>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<RouteButtonsDisplay
|
||||
alertReceiveChannelId={alertReceiveChannelId}
|
||||
channelFilterId={channelFilterId}
|
||||
routeIndex={routeIndex}
|
||||
setRouteIdForDeletion={() => setState({ routeIdForDeletion: channelFilterId })}
|
||||
/>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
}
|
||||
content={
|
||||
<VerticalGroup spacing="xs">
|
||||
{routeIndex !== channelFiltersTotal.length - 1 && (
|
||||
<IntegrationBlockItem>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<InlineLabel width={20} tooltip={'TODO: Add text'}>
|
||||
Routing Template
|
||||
</InlineLabel>
|
||||
<div className={cx('input', 'input--short')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(channelFilter.filtering_term, false)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
<Button variant={'secondary'} icon="edit" size={'md'} onClick={undefined} />
|
||||
<Button variant="secondary" size="md" onClick={() => openEditTemplateModal('routing')}>
|
||||
<Text type="link">Help</Text>
|
||||
<Icon name="angle-down" size="sm" />
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</IntegrationBlockItem>
|
||||
)}
|
||||
|
||||
{routeIndex !== channelFiltersTotal.length - 1 && (
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
<Text type="secondary">
|
||||
If the Routing template evaluates to True, the alert will be grouped with the Grouping template
|
||||
and proceed to the following steps
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
)}
|
||||
|
||||
{hasChatOpsConnectors && (
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup spacing="md">
|
||||
<Text type="primary">Publish to ChatOps</Text>
|
||||
<ChatOpsConnectors channelFilterId={channelFilterId} />
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
)}
|
||||
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<InlineLabel width={20}>Escalation chain</InlineLabel>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<GSelect
|
||||
showSearch
|
||||
modelName="escalationChainStore"
|
||||
isLoading={isRefreshingEscalationChains}
|
||||
displayField="name"
|
||||
placeholder="Select Escalation Chain"
|
||||
className={cx('select', 'control')}
|
||||
value={channelFilter.escalation_chain}
|
||||
onChange={onEscalationChainChange}
|
||||
showWarningIfEmptyValue={true}
|
||||
width={'auto'}
|
||||
icon={'list-ul'}
|
||||
getOptionLabel={(item: SelectableValue) => {
|
||||
return (
|
||||
<>
|
||||
<Text>{item.label} </Text>
|
||||
<TeamName
|
||||
team={grafanaTeamStore.items[escalationChainStore.items[item.value].team]}
|
||||
size="small"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</WithPermissionControlTooltip>
|
||||
|
||||
<Button variant={'secondary'} icon={'sync'} size={'md'} onClick={onEscalationChainsRefresh} />
|
||||
<PluginLink
|
||||
className={cx('hover-button')}
|
||||
target="_blank"
|
||||
query={{ page: 'escalations', id: channelFilter.escalation_chain }}
|
||||
>
|
||||
<Button variant={'secondary'} icon={'external-link-alt'} size={'md'} />
|
||||
</PluginLink>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
onClick={() => setState({ isEscalationCollapsed: !isEscalationCollapsed })}
|
||||
>
|
||||
<HorizontalGroup>
|
||||
<Text type="link">Show escalation chain</Text>
|
||||
{isEscalationCollapsed && <Icon name={'angle-right'} />}
|
||||
{!isEscalationCollapsed && <Icon name={'angle-up'} />}
|
||||
</HorizontalGroup>
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
|
||||
{isEscalationCollapsed && (
|
||||
<ReadOnlyEscalationChain escalationChainId={channelFilter.escalation_chain} />
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
</VerticalGroup>
|
||||
}
|
||||
/>
|
||||
{routeIdForDeletion && (
|
||||
<ConfirmModal
|
||||
isOpen
|
||||
title="Delete route?"
|
||||
body="Are you sure you want to delete this route?"
|
||||
confirmText="Delete"
|
||||
icon="exclamation-triangle"
|
||||
onConfirm={onRouteDeleteConfirm}
|
||||
onDismiss={() => setState({ routeIdForDeletion: undefined })}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
async function onRouteDeleteConfirm() {
|
||||
setState({ routeIdForDeletion: undefined });
|
||||
await alertReceiveChannelStore.deleteChannelFilter(routeIdForDeletion);
|
||||
}
|
||||
|
||||
function onEscalationChainChange(value: string) {
|
||||
alertReceiveChannelStore
|
||||
.saveChannelFilter(channelFilterId, {
|
||||
escalation_chain: value,
|
||||
})
|
||||
.then(() => {
|
||||
escalationChainStore.updateItems(); // to update number_of_integrations and number_of_routes
|
||||
escalationPolicyStore.updateEscalationPolicies(value);
|
||||
});
|
||||
}
|
||||
|
||||
async function onEscalationChainsRefresh() {
|
||||
setState({ isRefreshingEscalationChains: true });
|
||||
await escalationChainStore.updateItems();
|
||||
setState({ isRefreshingEscalationChains: false });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const ReadOnlyEscalationChain: React.FC<{ escalationChainId: string }> = ({ escalationChainId }) => {
|
||||
return <EscalationChainSteps isDisabled id={escalationChainId} />;
|
||||
};
|
||||
|
||||
interface RouteButtonsDisplayProps {
|
||||
alertReceiveChannelId: AlertReceiveChannel['id'];
|
||||
channelFilterId: ChannelFilter['id'];
|
||||
routeIndex: number;
|
||||
setRouteIdForDeletion(): void;
|
||||
}
|
||||
|
||||
export const RouteButtonsDisplay: React.FC<RouteButtonsDisplayProps> = ({
|
||||
alertReceiveChannelId,
|
||||
channelFilterId,
|
||||
routeIndex,
|
||||
setRouteIdForDeletion,
|
||||
}) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
const channelFilter = alertReceiveChannelStore.channelFilters[channelFilterId];
|
||||
const channelFiltersTotal = Object.keys(alertReceiveChannelStore.channelFilters);
|
||||
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
{routeIndex > 0 && !channelFilter.is_default && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Tooltip placement="top" content={'Move Up'}>
|
||||
<Button variant={'secondary'} onClick={onRouteMoveUp} icon={'arrow-up'} size={'xs'} />
|
||||
</Tooltip>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
{routeIndex < channelFiltersTotal.length - 2 && !channelFilter.is_default && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Tooltip placement="top" content={'Move Down'}>
|
||||
<Button variant={'secondary'} onClick={onRouteMoveDown} icon={'arrow-down'} size={'xs'} />
|
||||
</Tooltip>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
|
||||
{!channelFilter.is_default && (
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Tooltip placement="top" content={'Delete'}>
|
||||
<Button variant={'secondary'} icon={'trash-alt'} size={'xs'} onClick={setRouteIdForDeletion} />
|
||||
</Tooltip>
|
||||
</WithPermissionControlTooltip>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
|
||||
function onRouteMoveDown() {
|
||||
alertReceiveChannelStore.moveChannelFilterToPosition(alertReceiveChannelId, routeIndex, routeIndex + 1);
|
||||
}
|
||||
|
||||
function onRouteMoveUp() {
|
||||
alertReceiveChannelStore.moveChannelFilterToPosition(alertReceiveChannelId, routeIndex, routeIndex - 1);
|
||||
}
|
||||
};
|
||||
|
||||
export default ExpandedIntegrationRouteDisplay;
|
||||
143
grafana-plugin/src/pages/integration_2/Integration2.config.ts
Normal file
143
grafana-plugin/src/pages/integration_2/Integration2.config.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { KeyValuePair } from 'utils';
|
||||
|
||||
export const TEXTAREA_ROWS_COUNT = 4;
|
||||
export const MAX_CHARACTERS_COUNT = 50;
|
||||
|
||||
export const MONACO_OPTIONS = {
|
||||
renderLineHighlight: false,
|
||||
readOnly: true,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
handleMouseWheel: false,
|
||||
},
|
||||
hideCursorInOverviewRuler: true,
|
||||
minimap: { enabled: false },
|
||||
cursorStyle: {
|
||||
display: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
export const INTEGRATION_DEMO_PAYLOAD = {
|
||||
alert_uid: '08d6891a-835c-e661-39fa-96b6a9e26552',
|
||||
title: 'The whole system is down',
|
||||
image_url: 'https://http.cat/500',
|
||||
state: 'alerting',
|
||||
link_to_upstream_details: 'https://en.wikipedia.org/wiki/Downtime',
|
||||
message: 'Smth happened. Oh no!',
|
||||
};
|
||||
|
||||
export const MONACO_INPUT_HEIGHT_SMALL = '32px';
|
||||
export const MONACO_INPUT_HEIGHT_TALL = '120px';
|
||||
|
||||
const TemplateOptions = {
|
||||
SourceLink: new KeyValuePair('source_link_template', 'Source Link'),
|
||||
Autoacknowledge: new KeyValuePair('acknowledge_condition_template', 'Autoacknowledge'),
|
||||
Phone: new KeyValuePair('phone_call_title_template', 'Phone'),
|
||||
SMS: new KeyValuePair('sms_title_template', 'SMS'),
|
||||
SlackTitle: new KeyValuePair('slack_title_template', 'Title'),
|
||||
SlackMessage: new KeyValuePair('slack_message_template', 'Message'),
|
||||
SlackImage: new KeyValuePair('slack_image_url_template', 'Image'),
|
||||
EmailTitle: new KeyValuePair('email_title_template', 'Title'),
|
||||
EmailMessage: new KeyValuePair('email_message_template', 'Message'),
|
||||
TelegramTitle: new KeyValuePair('telegram_title_template', 'Title'),
|
||||
TelegramMessage: new KeyValuePair('telegram_message_template', 'Message'),
|
||||
TelegramImage: new KeyValuePair('telegram_image_url_template', 'Image'),
|
||||
/*Should it be in Oncallprivate repo? (All MsTeams)*/
|
||||
MSTeamsTitle: new KeyValuePair('MSTeams Title', 'Title'),
|
||||
MSTeamsMessage: new KeyValuePair('MSTeams Message', 'Message'),
|
||||
MSTeamsImage: new KeyValuePair('MSTeams Image', 'Image'),
|
||||
|
||||
Email: new KeyValuePair('Email', 'Email'),
|
||||
Slack: new KeyValuePair('Slack', 'Slack'),
|
||||
MSTeams: new KeyValuePair('Microsoft Teams', 'Microsoft Teams'),
|
||||
Telegram: new KeyValuePair('Telegram', 'Telegram'),
|
||||
};
|
||||
|
||||
export const INTEGRATION_TEMPLATES_LIST = [
|
||||
{
|
||||
label: TemplateOptions.SourceLink.value,
|
||||
value: TemplateOptions.SourceLink.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.Autoacknowledge.value,
|
||||
value: TemplateOptions.Autoacknowledge.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.Phone.value,
|
||||
value: TemplateOptions.Phone.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.SMS.value,
|
||||
value: TemplateOptions.SMS.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.Email.value,
|
||||
value: TemplateOptions.Email.key,
|
||||
children: [
|
||||
{
|
||||
label: TemplateOptions.EmailTitle.value,
|
||||
value: TemplateOptions.EmailTitle.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.EmailMessage.value,
|
||||
value: TemplateOptions.EmailMessage.key,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.Slack.value,
|
||||
value: TemplateOptions.Slack.key,
|
||||
children: [
|
||||
{
|
||||
label: TemplateOptions.SlackTitle.value,
|
||||
value: TemplateOptions.SlackTitle.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.SlackMessage.value,
|
||||
value: TemplateOptions.SlackMessage.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.SlackImage.value,
|
||||
value: TemplateOptions.SlackImage.key,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.MSTeams.value,
|
||||
value: TemplateOptions.MSTeams.key,
|
||||
children: [
|
||||
{
|
||||
label: TemplateOptions.MSTeamsTitle.value,
|
||||
value: TemplateOptions.MSTeamsTitle.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.MSTeamsMessage.value,
|
||||
value: TemplateOptions.MSTeamsMessage.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.MSTeamsImage.value,
|
||||
value: TemplateOptions.MSTeamsImage.key,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.Telegram.value,
|
||||
value: TemplateOptions.Telegram.key,
|
||||
children: [
|
||||
{
|
||||
label: TemplateOptions.TelegramTitle.value,
|
||||
value: TemplateOptions.TelegramTitle.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.TelegramMessage.value,
|
||||
value: TemplateOptions.TelegramMessage.key,
|
||||
},
|
||||
{
|
||||
label: TemplateOptions.TelegramImage.value,
|
||||
value: TemplateOptions.TelegramImage.key,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { ChannelFilter } from 'models/channel_filter';
|
||||
|
||||
import { MAX_CHARACTERS_COUNT, TEXTAREA_ROWS_COUNT } from './Integration2.config';
|
||||
|
||||
const IntegrationHelper = {
|
||||
getFilteredTemplate: (template: string, isTextArea: boolean): string => {
|
||||
if (!template) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lines = template.split('\n');
|
||||
if (isTextArea) {
|
||||
return lines
|
||||
.slice(0, TEXTAREA_ROWS_COUNT + 1)
|
||||
.map((line) => IntegrationHelper.truncateLine(line))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return IntegrationHelper.truncateLine(lines[0]);
|
||||
},
|
||||
|
||||
truncateLine: (line: string, maxCharacterCount: number = MAX_CHARACTERS_COUNT): string => {
|
||||
if (!line || !line.trim()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const slice = line.substring(0, maxCharacterCount);
|
||||
return slice.length === line.length ? slice : `${slice} ...`;
|
||||
},
|
||||
|
||||
getRouteConditionWording(channelFilters: { [id: string]: ChannelFilter }, routeIndex: number) {
|
||||
const totalCount = Object.keys(channelFilters).length;
|
||||
|
||||
if (routeIndex === totalCount - 1) {
|
||||
return 'Default';
|
||||
}
|
||||
return routeIndex ? 'Else' : 'If';
|
||||
},
|
||||
};
|
||||
|
||||
export default IntegrationHelper;
|
||||
105
grafana-plugin/src/pages/integration_2/Integration2.module.scss
Normal file
105
grafana-plugin/src/pages/integration_2/Integration2.module.scss
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
$FLEX-GAP: 4px;
|
||||
$MARGIN: 12px;
|
||||
$ITEMS-MARGIN: 24px;
|
||||
|
||||
.integration__heading-container {
|
||||
margin-bottom: $ITEMS-MARGIN;
|
||||
}
|
||||
|
||||
.integration__heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.integration__actions {
|
||||
display: flex;
|
||||
gap: $FLEX-GAP;
|
||||
}
|
||||
|
||||
.integration__actionsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 160px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.integration__actionItem {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
border-left: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
min-width: 84px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: row;
|
||||
|
||||
&:hover {
|
||||
background: var(--gray-9);
|
||||
}
|
||||
}
|
||||
|
||||
.integration__description {
|
||||
display: block;
|
||||
margin-bottom: $MARGIN;
|
||||
}
|
||||
|
||||
.hamburger-menu {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
justify-content: center;
|
||||
background-color: rgba(204, 204, 220, 0.16);
|
||||
color: var(--secondary-background);
|
||||
border: 1px solid transparent;
|
||||
height: 32px;
|
||||
width: 30px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.integration__counter {
|
||||
font-size: 12px;
|
||||
}
|
||||
.integration__countersBadge {
|
||||
line-height: 16px;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.loadingPlaceholder {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.customise-button button {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.routesSection {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.input {
|
||||
&--short {
|
||||
width: 500px;
|
||||
}
|
||||
&--long {
|
||||
width: 700px;
|
||||
}
|
||||
}
|
||||
|
||||
.how-to-connect__container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.tag,
|
||||
.how-to-connect__tag {
|
||||
height: 25px;
|
||||
}
|
||||
670
grafana-plugin/src/pages/integration_2/Integration2.tsx
Normal file
670
grafana-plugin/src/pages/integration_2/Integration2.tsx
Normal file
|
|
@ -0,0 +1,670 @@
|
|||
import React, { useRef } from 'react';
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
HorizontalGroup,
|
||||
VerticalGroup,
|
||||
Icon,
|
||||
LoadingPlaceholder,
|
||||
Tooltip,
|
||||
Modal,
|
||||
CascaderOption,
|
||||
} from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { get } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import Emoji from 'react-emoji-render';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import { TemplateForEdit, templateForEdit } from 'components/AlertTemplates/AlertTemplatesForm.config';
|
||||
import CounterBadge from 'components/CounterBadge/CounterBadge';
|
||||
import IntegrationCollapsibleTreeView, {
|
||||
IntegrationCollapsibleItem,
|
||||
} from 'components/IntegrationCollapsibleTreeView/IntegrationCollapsibleTreeView';
|
||||
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
|
||||
import IntegrationMaskedInputField from 'components/IntegrationMaskedInputField/IntegrationMaskedInputField';
|
||||
import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
|
||||
import { initErrorDataState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import SourceCode from 'components/SourceCode/SourceCode';
|
||||
import Tag from 'components/Tag/Tag';
|
||||
import Text from 'components/Text/Text';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import { WithContextMenu } from 'components/WithContextMenu/WithContextMenu';
|
||||
import IntegrationTemplate from 'containers/IntegrationTemplate/IntegrationTemplate';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
import UserDisplayWithAvatar from 'containers/UserDisplay/UserDisplayWithAvatar';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { AlertReceiveChannel } from 'models/alert_receive_channel';
|
||||
import { ChannelFilter } from 'models/channel_filter';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import { openNotification, openErrorNotification } from 'utils';
|
||||
import { getVar } from 'utils/DOM';
|
||||
import { UserActions } from 'utils/authorization';
|
||||
import { DATASOURCE_ALERTING, PLUGIN_ROOT } from 'utils/consts';
|
||||
|
||||
import CollapsedIntegrationRouteDisplay from './CollapsedIntegrationRouteDisplay';
|
||||
import ExpandedIntegrationRouteDisplay from './ExpandedIntegrationRouteDisplay';
|
||||
import { INTEGRATION_DEMO_PAYLOAD, INTEGRATION_TEMPLATES_LIST } from './Integration2.config';
|
||||
import IntegrationHelper from './Integration2.helper';
|
||||
import styles from './Integration2.module.scss';
|
||||
import IntegrationBlock from './IntegrationBlock';
|
||||
import IntegrationTemplateList from './IntegrationTemplatesList';
|
||||
// import { toJS } from 'mobx';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface Integration2Props extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {}
|
||||
|
||||
interface Integration2State extends PageBaseState {
|
||||
isDemoModalOpen: boolean;
|
||||
isEditTemplateModalOpen: boolean;
|
||||
selectedTemplate: TemplateForEdit;
|
||||
}
|
||||
|
||||
// This can be further improved by using a ref instead
|
||||
const ACTIONS_LIST_WIDTH = 160;
|
||||
const ACTIONS_LIST_BORDER = 2;
|
||||
|
||||
@observer
|
||||
class Integration2 extends React.Component<Integration2Props, Integration2State> {
|
||||
constructor(props: Integration2Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
errorData: initErrorDataState(),
|
||||
isDemoModalOpen: false,
|
||||
isEditTemplateModalOpen: false,
|
||||
selectedTemplate: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const {
|
||||
match: {
|
||||
params: { id },
|
||||
},
|
||||
} = this.props;
|
||||
const {
|
||||
store: { alertReceiveChannelStore },
|
||||
} = this.props;
|
||||
|
||||
await Promise.all([this.loadIntegration(), alertReceiveChannelStore.updateTemplates(id)]);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { errorData, isDemoModalOpen, isEditTemplateModalOpen, selectedTemplate } = this.state;
|
||||
const {
|
||||
store: { alertReceiveChannelStore, grafanaTeamStore },
|
||||
match: {
|
||||
params: { id },
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const { isNotFoundError, isWrongTeamError } = errorData;
|
||||
|
||||
const alertReceiveChannel = alertReceiveChannelStore.items[id];
|
||||
const channelFilterIds = alertReceiveChannelStore.channelFilterIds[id];
|
||||
const templates = alertReceiveChannelStore.templates[id];
|
||||
|
||||
if ((!alertReceiveChannel && !isNotFoundError && !isWrongTeamError) || !channelFilterIds || !templates) {
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<LoadingPlaceholder text="Loading Integration..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const integration = alertReceiveChannelStore.getIntegration(alertReceiveChannel);
|
||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[id];
|
||||
|
||||
return (
|
||||
<PageErrorHandlingWrapper errorData={errorData} objectName="integration" pageName="Integration">
|
||||
{() => (
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('integration__heading-container')}>
|
||||
<div className={cx('integration__heading')}>
|
||||
<h1 className={cx('integration__name')}>
|
||||
<Emoji text={alertReceiveChannel.verbal_name} />
|
||||
</h1>
|
||||
<div className={cx('integration__actions')}>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsTest}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="md"
|
||||
onClick={() => this.setState({ isDemoModalOpen: true })}
|
||||
data-testid="send-demo-alert"
|
||||
>
|
||||
Send demo alert
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
|
||||
<WithContextMenu
|
||||
renderMenuItems={({ closeMenu }) => (
|
||||
<div className={cx('integration__actionsList')} id="integration-menu-options">
|
||||
<div
|
||||
className={cx('integration__actionItem')}
|
||||
onClick={() => this.openIntegrationSettings(id, closeMenu)}
|
||||
>
|
||||
<Text type="primary">Integration Settings</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('integration__actionItem')} onClick={() => this.openHearbeat(id, closeMenu)}>
|
||||
Hearbeat
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cx('integration__actionItem')}
|
||||
onClick={() => this.openStartMaintenance(id, closeMenu)}
|
||||
>
|
||||
<Text type="primary">Start Maintenance</Text>
|
||||
</div>
|
||||
|
||||
<div className="thin-line-break" />
|
||||
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<div className={cx('integration__actionItem')}>
|
||||
<WithConfirm
|
||||
title="Delete integration?"
|
||||
body={
|
||||
<>
|
||||
Are you sure you want to delete <Emoji text={alertReceiveChannel.verbal_name} />{' '}
|
||||
integration?
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div onClick={() => this.deleteIntegration(id, closeMenu)}>
|
||||
<div
|
||||
onClick={() => {
|
||||
// work-around to prevent 2 modals showing (withContextMenu and ConfirmModal)
|
||||
const contextMenuEl =
|
||||
document.querySelector<HTMLElement>('#integration-menu-options');
|
||||
if (contextMenuEl) {
|
||||
contextMenuEl.style.display = 'none';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text type="danger">Stop Maintenance</Text>
|
||||
</div>
|
||||
</div>
|
||||
</WithConfirm>
|
||||
</div>
|
||||
</WithPermissionControlTooltip>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{({ openMenu }) => <HamburgerMenu openMenu={openMenu} />}
|
||||
</WithContextMenu>
|
||||
</div>
|
||||
</div>
|
||||
{alertReceiveChannel.description && (
|
||||
<Text type="secondary" className={cx('integration__description')}>
|
||||
{alertReceiveChannel.description}
|
||||
</Text>
|
||||
)}
|
||||
<HorizontalGroup>
|
||||
{alertReceiveChannelCounter && (
|
||||
<Tooltip
|
||||
placement="bottom-start"
|
||||
content={
|
||||
alertReceiveChannelCounter?.alerts_count +
|
||||
' alert' +
|
||||
(alertReceiveChannelCounter?.alerts_count === 1 ? '' : 's') +
|
||||
' in ' +
|
||||
alertReceiveChannelCounter?.alert_groups_count +
|
||||
' alert group' +
|
||||
(alertReceiveChannelCounter?.alert_groups_count === 1 ? '' : 's')
|
||||
}
|
||||
>
|
||||
{/* <span> is needed to be child, otherwise Tooltip won't render */}
|
||||
<span>
|
||||
<PluginLink
|
||||
query={{ page: 'alert-groups', integration: alertReceiveChannel.id }}
|
||||
className={cx('integration__counter')}
|
||||
>
|
||||
<Badge
|
||||
text={
|
||||
alertReceiveChannelCounter?.alerts_count +
|
||||
'/' +
|
||||
alertReceiveChannelCounter?.alert_groups_count
|
||||
}
|
||||
className={cx('integration__countersBadge')}
|
||||
color={'blue'}
|
||||
/>
|
||||
</PluginLink>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<CounterBadge
|
||||
borderType="success"
|
||||
icon="link"
|
||||
count={channelFilterIds.length}
|
||||
tooltipTitle={`${channelFilterIds.length} Routes`}
|
||||
tooltipContent={undefined}
|
||||
/>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Text type="secondary">Type:</Text>
|
||||
<HorizontalGroup spacing="none">
|
||||
<IntegrationLogo scale={0.08} integration={integration} />
|
||||
<Text type="secondary" size="small">
|
||||
{integration?.display_name}
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Text type="secondary">Team:</Text>
|
||||
<TeamName team={grafanaTeamStore.items[alertReceiveChannel.team]} size="small" />
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="xs">
|
||||
<Text type="secondary">Created by:</Text>
|
||||
<UserDisplayWithAvatar id={alertReceiveChannel.author as any}></UserDisplayWithAvatar>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
|
||||
<IntegrationCollapsibleTreeView
|
||||
configElements={[
|
||||
{
|
||||
isCollapsible: false,
|
||||
customIcon: 'plug',
|
||||
collapsedView: null,
|
||||
expandedView: <HowToConnectComponent id={id} />,
|
||||
},
|
||||
{
|
||||
isCollapsible: true,
|
||||
collapsedView: (
|
||||
<IntegrationBlock
|
||||
hasCollapsedBorder
|
||||
heading={
|
||||
<HorizontalGroup spacing={'md'}>
|
||||
<Tag color={getVar('--tag-secondary')} className={cx('tag')}>
|
||||
<Text type="primary" size="small">
|
||||
Templates
|
||||
</Text>
|
||||
</Tag>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Text type="secondary">Grouping:</Text>
|
||||
<Text type="link">
|
||||
{IntegrationHelper.getFilteredTemplate(templates['grouping_id_template'] || '', false)}
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Text type="secondary">Visualisation:</Text>
|
||||
<Text type="primary">Multiple</Text>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
}
|
||||
content={null}
|
||||
/>
|
||||
),
|
||||
expandedView: (
|
||||
<IntegrationBlock
|
||||
hasCollapsedBorder
|
||||
heading={
|
||||
<HorizontalGroup>
|
||||
<Tag color={getVar('--tag-secondary')} className={cx('tag')}>
|
||||
<Text type="primary" size="small">
|
||||
Templates
|
||||
</Text>
|
||||
</Tag>
|
||||
</HorizontalGroup>
|
||||
}
|
||||
content={
|
||||
<IntegrationTemplateList
|
||||
getTemplatesList={this.getTemplatesList}
|
||||
openEditTemplateModal={this.openEditTemplateModal}
|
||||
templates={templates}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
customIcon: 'plus',
|
||||
isCollapsible: false,
|
||||
collapsedView: null,
|
||||
expandedView: (
|
||||
<div className={cx('routesSection')}>
|
||||
<VerticalGroup spacing="md">
|
||||
<Text type={'primary'}>Routes</Text>
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Button variant={'primary'} onClick={() => this.openEditTemplateModal('routing')}>
|
||||
Add route
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
this.renderRoutesFn(),
|
||||
]}
|
||||
/>
|
||||
|
||||
<IntegrationSendDemoPayloadModal
|
||||
alertReceiveChannel={alertReceiveChannel}
|
||||
isOpen={isDemoModalOpen}
|
||||
onHideOrCancel={() => this.setState({ isDemoModalOpen: false })}
|
||||
/>
|
||||
{isEditTemplateModalOpen && (
|
||||
<IntegrationTemplate
|
||||
id={id}
|
||||
onHide={() => {
|
||||
this.setState({
|
||||
isEditTemplateModalOpen: undefined,
|
||||
});
|
||||
}}
|
||||
onUpdateTemplates={this.onUpdateTemplatesCallback}
|
||||
onUpdateRoute={this.onUpdateRoutesCallback}
|
||||
template={selectedTemplate}
|
||||
templateBody={templates[selectedTemplate?.name]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PageErrorHandlingWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
renderRoutesFn = (): IntegrationCollapsibleItem[] => {
|
||||
const {
|
||||
store: { alertReceiveChannelStore },
|
||||
match: {
|
||||
params: { id },
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const templates = alertReceiveChannelStore.templates[id];
|
||||
const channelFilterIds = alertReceiveChannelStore.channelFilterIds[id];
|
||||
|
||||
return channelFilterIds.map((channelFilterId: ChannelFilter['id'], routeIndex: number) => ({
|
||||
isCollapsible: true,
|
||||
collapsedView: (
|
||||
<CollapsedIntegrationRouteDisplay
|
||||
alertReceiveChannelId={id}
|
||||
channelFilterId={channelFilterId}
|
||||
routeIndex={routeIndex}
|
||||
/>
|
||||
),
|
||||
expandedView: (
|
||||
<ExpandedIntegrationRouteDisplay
|
||||
alertReceiveChannelId={id}
|
||||
channelFilterId={channelFilterId}
|
||||
routeIndex={routeIndex}
|
||||
templates={templates}
|
||||
openEditTemplateModal={this.openEditTemplateModal}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
handleSlackChannelChange = () => {};
|
||||
|
||||
onUpdateRoutesCallback = ({ routing }: { routing: string }) => {
|
||||
const { alertReceiveChannelStore, escalationPolicyStore } = this.props.store;
|
||||
const {
|
||||
params: { id },
|
||||
} = this.props.match;
|
||||
|
||||
alertReceiveChannelStore
|
||||
.createChannelFilter({
|
||||
order: 0,
|
||||
alert_receive_channel: id,
|
||||
filtering_term: routing,
|
||||
|
||||
// TODO: need to figure out this value
|
||||
filtering_term_type: 1,
|
||||
})
|
||||
.then((channelFilter: ChannelFilter) => {
|
||||
alertReceiveChannelStore.updateChannelFilters(id, true).then(() => {
|
||||
// @ts-ignore
|
||||
escalationPolicyStore.updateEscalationPolicies(channelFilter.escalation_chain);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
const errors = get(err, 'response.data');
|
||||
if (errors?.non_field_errors) {
|
||||
openErrorNotification(errors.non_field_errors);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onUpdateTemplatesCallback = (data) => {
|
||||
const {
|
||||
store,
|
||||
match: {
|
||||
params: { id },
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
store.alertReceiveChannelStore
|
||||
.saveTemplates(id, data)
|
||||
.then(() => {
|
||||
openNotification('The Alert templates have been updated');
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.response?.data?.length > 0) {
|
||||
openErrorNotification(err.response.data);
|
||||
} else {
|
||||
openErrorNotification(err.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getTemplatesList = (): CascaderOption[] => INTEGRATION_TEMPLATES_LIST;
|
||||
|
||||
openEditTemplateModal = (templateName) => {
|
||||
this.setState({ isEditTemplateModalOpen: true });
|
||||
this.setState({ selectedTemplate: templateForEdit[templateName] });
|
||||
};
|
||||
|
||||
onRemovalFn = (id: AlertReceiveChannel['id']) => {
|
||||
const {
|
||||
store: { alertReceiveChannelStore },
|
||||
history,
|
||||
} = this.props;
|
||||
|
||||
alertReceiveChannelStore.deleteAlertReceiveChannel(id).then(() => history.push(`${PLUGIN_ROOT}/integrations_2/`));
|
||||
};
|
||||
|
||||
deleteIntegration = (_id: AlertReceiveChannel['id'], _closeMenu: () => void) => {};
|
||||
|
||||
openIntegrationSettings = (_id: AlertReceiveChannel['id'], _closeMenu: () => void) => {};
|
||||
|
||||
openStartMaintenance = (_id: AlertReceiveChannel['id'], _closeMenu: () => void) => {};
|
||||
|
||||
openHearbeat = (_id: AlertReceiveChannel['id'], _closeMenu: () => void) => {};
|
||||
|
||||
async loadIntegration() {
|
||||
const {
|
||||
store: { alertReceiveChannelStore },
|
||||
match: {
|
||||
params: { id },
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
const promises = [];
|
||||
|
||||
if (!alertReceiveChannelStore.items[id]) {
|
||||
// See what happens if the request fails
|
||||
promises.push(alertReceiveChannelStore.loadItem(id));
|
||||
}
|
||||
|
||||
if (!alertReceiveChannelStore.counters?.length) {
|
||||
promises.push(alertReceiveChannelStore.updateCounters());
|
||||
}
|
||||
|
||||
if (!alertReceiveChannelStore.channelFilterIds[id]) {
|
||||
promises.push(await alertReceiveChannelStore.updateChannelFilters(id));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
const DemoNotification: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
Demo alert was generated. Find it on the
|
||||
<PluginLink query={{ page: 'alert-groups' }}> "Alert Groups" </PluginLink>
|
||||
page and make sure it didn't freak out your colleagues 😉
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const HamburgerMenu: React.FC<{ openMenu: React.MouseEventHandler<HTMLElement> }> = ({ openMenu }) => {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cx('hamburger-menu')}
|
||||
onClick={() => {
|
||||
const boundingRect = ref.current.getBoundingClientRect();
|
||||
|
||||
openMenu({
|
||||
pageX: boundingRect.right - ACTIONS_LIST_WIDTH + ACTIONS_LIST_BORDER * 2,
|
||||
pageY: boundingRect.top + boundingRect.height,
|
||||
} as any);
|
||||
}}
|
||||
>
|
||||
<Icon size="sm" name="ellipsis-v" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IntegrationSendDemoPayloadModalProps {
|
||||
isOpen: boolean;
|
||||
alertReceiveChannel: AlertReceiveChannel;
|
||||
onHideOrCancel: () => void;
|
||||
}
|
||||
|
||||
const IntegrationSendDemoPayloadModal: React.FC<IntegrationSendDemoPayloadModalProps> = ({
|
||||
alertReceiveChannel,
|
||||
isOpen,
|
||||
onHideOrCancel,
|
||||
}) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closeOnEscape
|
||||
isOpen={isOpen}
|
||||
onDismiss={onHideOrCancel}
|
||||
title={`Send demo alert to ${alertReceiveChannel.verbal_name}`}
|
||||
>
|
||||
<VerticalGroup>
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Text type={'secondary'}>Alert Payload</Text>
|
||||
<Tooltip content={'TODO'} placement={'top-start'}>
|
||||
<Icon name={'info-circle'} />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
|
||||
<SourceCode showCopyToClipboard={false}>{getDemoAlertJSON()}</SourceCode>
|
||||
|
||||
<HorizontalGroup justify={'flex-end'} spacing={'md'}>
|
||||
<Button variant={'secondary'} onClick={onHideOrCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<CopyToClipboard text={getCurlText()} onCopy={() => openNotification('CURL copied!')}>
|
||||
<Button variant={'secondary'}>Copy as CURL</Button>
|
||||
</CopyToClipboard>
|
||||
<Button variant={'primary'} onClick={sendDemoAlert}>
|
||||
Send Alert
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
function sendDemoAlert() {
|
||||
alertReceiveChannelStore.sendDemoAlert(alertReceiveChannel.id).then(() => {
|
||||
alertReceiveChannelStore.updateCounters();
|
||||
openNotification(<DemoNotification />);
|
||||
onHideOrCancel();
|
||||
});
|
||||
}
|
||||
|
||||
function getCurlText() {
|
||||
// TODO add this
|
||||
return `curl -X POST [URL]
|
||||
-H "Content-Type: application/json"
|
||||
-d "[JSON data]"`;
|
||||
}
|
||||
|
||||
function getDemoAlertJSON() {
|
||||
return JSON.stringify(INTEGRATION_DEMO_PAYLOAD, null, 4);
|
||||
}
|
||||
};
|
||||
|
||||
const HowToConnectComponent: React.FC<{ id: AlertReceiveChannel['id'] }> = ({ id }) => {
|
||||
const { alertReceiveChannelStore } = useStore();
|
||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[id];
|
||||
const alertReceiveChannel = alertReceiveChannelStore.items[id];
|
||||
const isAlertManager = alertReceiveChannel.integration === DATASOURCE_ALERTING;
|
||||
const hasAlerts = !!alertReceiveChannelCounter?.alerts_count;
|
||||
|
||||
return (
|
||||
<IntegrationBlock
|
||||
hasCollapsedBorder={false}
|
||||
heading={
|
||||
<div className={cx('how-to-connect__container')}>
|
||||
<Tag color={getVar('--tag-secondary')} className={cx('how-to-connect__tag')}>
|
||||
<Text type="primary" size="small">
|
||||
HTTP Endpoint
|
||||
</Text>
|
||||
</Tag>
|
||||
<IntegrationMaskedInputField value={alertReceiveChannelStore.items[id].integration_url} />
|
||||
<a href="https://grafana.com/docs/oncall/latest/integrations/" target="_blank" rel="noreferrer">
|
||||
<Text type="link" size="small" onClick={openHowToConnect}>
|
||||
<HorizontalGroup>
|
||||
How to connect
|
||||
<Icon name="external-link-alt" />
|
||||
</HorizontalGroup>
|
||||
</Text>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
content={isAlertManager || !hasAlerts ? renderContent() : null}
|
||||
/>
|
||||
);
|
||||
|
||||
function openHowToConnect() {}
|
||||
|
||||
function renderContent() {
|
||||
return (
|
||||
<div className={cx('integration__alertsPanel')}>
|
||||
<VerticalGroup justify={'flex-start'} spacing={'xs'}>
|
||||
{!hasAlerts && (
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<LoadingPlaceholder text="" className={cx('loadingPlaceholder')} />
|
||||
<Text type={'primary'}>No alerts yet; try to send a demo alert</Text>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
|
||||
{isAlertManager && (
|
||||
<HorizontalGroup spacing={'xs'}>
|
||||
<Icon name="list-ui-alt" size="md" />
|
||||
<a href="/alerting/notifications" target="_blank">
|
||||
<Text type={'link'}>Contact Point</Text>
|
||||
</a>
|
||||
<Text type={'secondary'}>and</Text>
|
||||
<a href="/alerting/routes" target="_blank">
|
||||
<Text type={'link'}>Notification Policy</Text>
|
||||
</a>
|
||||
<Text type={'secondary'}>created in Grafana Alerting</Text>
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default withRouter(withMobXProviderContext(Integration2));
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
.integrationBlock__heading,
|
||||
.integrationBlock__content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.integrationBlock__heading {
|
||||
background-color: var(--background-secondary);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.integrationBlock__content {
|
||||
background: var(--background-primary);
|
||||
border: var(--border-weak);
|
||||
|
||||
&--collapsedBorder {
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
36
grafana-plugin/src/pages/integration_2/IntegrationBlock.tsx
Normal file
36
grafana-plugin/src/pages/integration_2/IntegrationBlock.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import Block from 'components/GBlock/Block';
|
||||
|
||||
import styles from './IntegrationBlock.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface IntegrationBlockProps {
|
||||
hasCollapsedBorder: boolean;
|
||||
heading: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
const IntegrationBlock: React.FC<IntegrationBlockProps> = ({ heading, content, hasCollapsedBorder }) => {
|
||||
return (
|
||||
<div className={cx('integrationBlock')}>
|
||||
<Block bordered shadowed className={cx('integrationBlock__heading')}>
|
||||
{heading}
|
||||
</Block>
|
||||
{content && (
|
||||
<div
|
||||
className={cx('integrationBlock__content', {
|
||||
'integrationBlock__content--collapsedBorder': hasCollapsedBorder,
|
||||
})}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntegrationBlock;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
.blockItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&__content {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
&__leftDelimitator {
|
||||
border-left: var(--border-medium);
|
||||
border-left-width: 3px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import styles from './IntegrationBlockItem.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface IntegrationBlockItemProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const IntegrationBlockItem: React.FC<IntegrationBlockItemProps> = (props) => {
|
||||
return (
|
||||
<div className={cx('blockItem')}>
|
||||
<div className={cx('blockItem__leftDelimitator')} />
|
||||
<div className={cx('blockItem__content')}>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntegrationBlockItem;
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button, HorizontalGroup, Icon, InlineLabel } from '@grafana/ui';
|
||||
|
||||
import Text from 'components/Text/Text';
|
||||
|
||||
interface IntegrationTemplateBlockProps {
|
||||
label: string;
|
||||
labelTooltip?: string;
|
||||
renderInput: () => React.ReactNode;
|
||||
showClose?: boolean;
|
||||
showHelp?: boolean;
|
||||
|
||||
onEdit: (templateName) => void;
|
||||
onRemove?: () => void;
|
||||
onHelp?: () => void;
|
||||
}
|
||||
|
||||
const IntegrationTemplateBlock: React.FC<IntegrationTemplateBlockProps> = ({
|
||||
label,
|
||||
labelTooltip,
|
||||
renderInput,
|
||||
showClose,
|
||||
showHelp,
|
||||
onEdit,
|
||||
onHelp,
|
||||
onRemove,
|
||||
}) => {
|
||||
let inlineLabelProps = { labelTooltip };
|
||||
if (!labelTooltip) {
|
||||
delete inlineLabelProps.labelTooltip;
|
||||
}
|
||||
|
||||
return (
|
||||
<HorizontalGroup align={'flex-start'}>
|
||||
<InlineLabel width={20} {...inlineLabelProps}>
|
||||
{label}
|
||||
</InlineLabel>
|
||||
{renderInput()}
|
||||
<Button variant={'secondary'} icon={'edit'} size={'md'} onClick={onEdit} />
|
||||
{showClose && <Button variant={'secondary'} icon={'times'} size={'md'} onClick={onRemove} />}
|
||||
{showHelp && (
|
||||
<Button variant="secondary" size="md" onClick={onHelp}>
|
||||
<Text type="link">Help</Text>
|
||||
<Icon name="angle-down" size="sm" />
|
||||
</Button>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntegrationTemplateBlock;
|
||||
|
|
@ -0,0 +1,448 @@
|
|||
import React from 'react';
|
||||
|
||||
import { ButtonCascader, CascaderOption, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import MonacoJinja2Editor from 'components/MonacoJinja2Editor/MonacoJinja2Editor';
|
||||
import Text from 'components/Text/Text';
|
||||
import { AlertTemplatesDTO } from 'models/alert_templates';
|
||||
|
||||
import { MONACO_INPUT_HEIGHT_SMALL, MONACO_INPUT_HEIGHT_TALL, MONACO_OPTIONS } from './Integration2.config';
|
||||
import IntegrationHelper from './Integration2.helper';
|
||||
import styles from './Integration2.module.scss';
|
||||
import IntegrationBlockItem from './IntegrationBlockItem';
|
||||
import IntegrationTemplateBlock from './IntegrationTemplateBlock';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface IntegrationTemplateListProps {
|
||||
templates: AlertTemplatesDTO[];
|
||||
getTemplatesList(): CascaderOption[];
|
||||
openEditTemplateModal: (templateName: string | string[]) => void;
|
||||
}
|
||||
|
||||
const IntegrationTemplateList: React.FC<IntegrationTemplateListProps> = ({
|
||||
templates,
|
||||
openEditTemplateModal,
|
||||
getTemplatesList,
|
||||
}) => {
|
||||
const isAutoAcknOrSourceLinkChanged =
|
||||
!templates['acknowledge_condition_template_is_default'] || !templates['source_link_template'];
|
||||
const isPhoneOrSMSChanged =
|
||||
!templates['sms_title_template_is_default'] || !templates['phone_call_title_template_is_default'];
|
||||
const isSlackChanged =
|
||||
!templates['slack_title_template_is_default'] ||
|
||||
!templates['slack_message_template_is_default'] ||
|
||||
!templates['slack_image_url_template_is_default'];
|
||||
const isTelegramChanged =
|
||||
!templates['telegram_title_template_is_default'] ||
|
||||
!templates['telegram_message_template_is_default'] ||
|
||||
!templates['telegram_image_url_template_is_default'];
|
||||
const isEmailOrMessageChanged = !templates['email_title_template_is_default'] || !templates['email_message_template'];
|
||||
|
||||
return (
|
||||
<div className={cx('integration__templates')}>
|
||||
<IntegrationBlockItem>
|
||||
<Text type="secondary">
|
||||
Templates are used to interpret alert from monitoring. Reduce noise, customize visualization
|
||||
</Text>
|
||||
</IntegrationBlockItem>
|
||||
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
<IntegrationTemplateBlock
|
||||
label={'Grouping'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--short')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['grouping_id_template'] || '', false)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
showHelp
|
||||
onEdit={() => openEditTemplateModal('grouping_id_template')}
|
||||
/>
|
||||
|
||||
<IntegrationTemplateBlock
|
||||
label={'Auto resolve'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--short')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['resolve_condition_template'] || '', false)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('resolve_condition_template')}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
<Text type={'primary'}>Web</Text>
|
||||
|
||||
<IntegrationTemplateBlock
|
||||
label={'Title'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--long')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['web_title_template'] || '', true)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_TALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('web_title_template')}
|
||||
/>
|
||||
|
||||
<IntegrationTemplateBlock
|
||||
label={'Message'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--long')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['web_message_template'] || '', true)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_TALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('web_message_template')}
|
||||
/>
|
||||
|
||||
<IntegrationTemplateBlock
|
||||
label={'Image'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--long')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['web_image_url_template'] || '', false)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('web_image_url_template')}
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
|
||||
{isAutoAcknOrSourceLinkChanged && (
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
{!templates['acknowledge_condition_template_is_default'] && (
|
||||
<IntegrationTemplateBlock
|
||||
label={'Auto acknowledge'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--short')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(
|
||||
templates['acknowledge_condition_template'] || '',
|
||||
false
|
||||
)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('acknowledge_condition_template')}
|
||||
showHelp
|
||||
/>
|
||||
)}
|
||||
|
||||
{!templates['source_link_template_is_default'] && (
|
||||
<IntegrationTemplateBlock
|
||||
label={'Source Link'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--short')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['source_link_template'] || '', false)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('source_link_template')}
|
||||
/>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
)}
|
||||
|
||||
{isPhoneOrSMSChanged && (
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
{!templates['phone_call_title_template_is_default'] && (
|
||||
<IntegrationTemplateBlock
|
||||
label={'Phone Call'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--short')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['phone_call_title_template'] || '', false)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('phone_call_title_template')}
|
||||
showHelp
|
||||
/>
|
||||
)}
|
||||
|
||||
{!templates['sms_title_template_is_default'] && (
|
||||
<IntegrationTemplateBlock
|
||||
label={'SMS'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--short')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['sms_title_template'] || '', false)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('sms_title_template')}
|
||||
/>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
)}
|
||||
|
||||
{isSlackChanged && (
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
<Text type={'primary'}>Slack</Text>
|
||||
|
||||
{!templates['slack_title_template_is_default'] && (
|
||||
<IntegrationTemplateBlock
|
||||
label={'Title'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--long')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['slack_title_template'] || '', false)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('slack_title_template')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!templates['slack_message_template_is_default'] && (
|
||||
<IntegrationTemplateBlock
|
||||
label={'Message'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--long')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['slack_message_template'] || '', true)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_TALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('slack_message_template')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!templates['slack_image_url_template_is_default'] && (
|
||||
<IntegrationTemplateBlock
|
||||
label={'Image'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--long')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['slack_image_url_template'] || '', false)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('slack_image_url_template')}
|
||||
/>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
)}
|
||||
|
||||
{isTelegramChanged && (
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
<Text type={'primary'}>Telegram</Text>
|
||||
{!templates['telegram_title_template_is_default'] && (
|
||||
<IntegrationTemplateBlock
|
||||
label={'Title'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--long')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['telegram_title_template'] || '', false)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('telegram_title_template')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!templates['telegram_message_template_is_default'] && (
|
||||
<IntegrationTemplateBlock
|
||||
label={'Message'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--long')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['telegram_message_template'] || '', true)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_TALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('telegram_message_template')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!templates['telegram_image_url_template_is_default'] && (
|
||||
<IntegrationTemplateBlock
|
||||
label={'Image'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--long')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(
|
||||
templates['telegram_image_url_template'] || '',
|
||||
false
|
||||
)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('telegram_image_url_template')}
|
||||
/>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
)}
|
||||
|
||||
{isEmailOrMessageChanged && (
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
<Text type={'primary'}>Email</Text>
|
||||
{!templates['email_title_template_is_default'] && (
|
||||
<IntegrationTemplateBlock
|
||||
label={'Title'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--long')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['email_title_template'] || '', false)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_SMALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('email_title_template')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!templates['email_message_template_is_default'] && (
|
||||
<IntegrationTemplateBlock
|
||||
label={'Message'}
|
||||
renderInput={() => (
|
||||
<div className={cx('input', 'input--long')}>
|
||||
<MonacoJinja2Editor
|
||||
value={IntegrationHelper.getFilteredTemplate(templates['email_message_template'] || '', true)}
|
||||
disabled={true}
|
||||
height={MONACO_INPUT_HEIGHT_TALL}
|
||||
data={templates}
|
||||
showLineNumbers={false}
|
||||
monacoOptions={MONACO_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onEdit={() => openEditTemplateModal('email_message_template')}
|
||||
/>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
)}
|
||||
|
||||
<IntegrationBlockItem>
|
||||
<VerticalGroup>
|
||||
<Text type={'secondary'}>By default alert groups rendered based on Web templates.</Text>
|
||||
<Text type={'secondary'}>
|
||||
Customise how they rendered in SMS, Phone Calls, Mobile App, Slack, Telegram, MS Teams{' '}
|
||||
</Text>
|
||||
|
||||
<div className={cx('customise-button')}>
|
||||
<ButtonCascader
|
||||
variant="secondary"
|
||||
onChange={(_key) => {
|
||||
if (Object.values(_key).length > 1) {
|
||||
openEditTemplateModal(Object.values(_key)[1]);
|
||||
} else {
|
||||
openEditTemplateModal(_key);
|
||||
}
|
||||
}}
|
||||
options={getTemplatesList()}
|
||||
icon="plus"
|
||||
value={undefined}
|
||||
buttonProps={{ size: 'sm' }}
|
||||
>
|
||||
Customise templates
|
||||
</ButtonCascader>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</IntegrationBlockItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntegrationTemplateList;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.newIntegrationButton {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 24px;
|
||||
right: 0;
|
||||
}
|
||||
407
grafana-plugin/src/pages/integrations_2/Integrations2.tsx
Normal file
407
grafana-plugin/src/pages/integrations_2/Integrations2.tsx
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
import React from 'react';
|
||||
|
||||
import { HorizontalGroup, Badge, Tooltip, Button, IconButton } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { observer } from 'mobx-react';
|
||||
import Emoji from 'react-emoji-render';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import GTable from 'components/GTable/GTable';
|
||||
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
|
||||
import { Filters } from 'components/IntegrationsFilters/IntegrationsFilters';
|
||||
import { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
|
||||
import {
|
||||
getWrongTeamResponseInfo,
|
||||
initErrorDataState,
|
||||
} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import IntegrationForm from 'containers/IntegrationForm/IntegrationForm';
|
||||
import RemoteFilters from 'containers/RemoteFilters/RemoteFilters';
|
||||
import TeamName from 'containers/TeamName/TeamName';
|
||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||
import { HeartGreenIcon, HeartRedIcon } from 'icons';
|
||||
import { AlertReceiveChannel, MaintenanceMode } from 'models/alert_receive_channel';
|
||||
import { MaintenanceType } from 'models/maintenance/maintenance.types';
|
||||
import { PageProps, WithStoreProps } from 'state/types';
|
||||
import { withMobXProviderContext } from 'state/withStore';
|
||||
import LocationHelper from 'utils/LocationHelper';
|
||||
import { UserActions, isUserActionAllowed } from 'utils/authorization';
|
||||
|
||||
import styles from './Integrations2.module.scss';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
const FILTERS_DEBOUNCE_MS = 500;
|
||||
// const ITEMS_PER_PAGE = 25;
|
||||
|
||||
interface IntegrationsState extends PageBaseState {
|
||||
integrationsFilters: Filters;
|
||||
alertReceiveChannelId?: AlertReceiveChannel['id'] | 'new';
|
||||
page: number;
|
||||
}
|
||||
|
||||
interface IntegrationsProps extends WithStoreProps, PageProps, RouteComponentProps<{ id: string }> {}
|
||||
|
||||
@observer
|
||||
class Integrations extends React.Component<IntegrationsProps, IntegrationsState> {
|
||||
state: IntegrationsState = {
|
||||
integrationsFilters: { searchTerm: '' },
|
||||
errorData: initErrorDataState(),
|
||||
page: 1,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
const {
|
||||
query: { p },
|
||||
} = this.props;
|
||||
this.setState({ page: p ? Number(p) : 1 }, this.update);
|
||||
|
||||
this.parseQueryParams();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IntegrationsProps) {
|
||||
if (prevProps.match.params.id !== this.props.match.params.id) {
|
||||
this.parseQueryParams();
|
||||
}
|
||||
}
|
||||
|
||||
parseQueryParams = async () => {
|
||||
this.setState((_prevState) => ({
|
||||
errorData: initErrorDataState(),
|
||||
alertReceiveChannelId: undefined,
|
||||
})); // reset state on query parse
|
||||
|
||||
const {
|
||||
store,
|
||||
match: {
|
||||
params: { id },
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let alertReceiveChannel: AlertReceiveChannel | void = undefined;
|
||||
const isNewAlertReceiveChannel = id === 'new';
|
||||
|
||||
if (!isNewAlertReceiveChannel) {
|
||||
alertReceiveChannel = await store.alertReceiveChannelStore
|
||||
.loadItem(id, true)
|
||||
.catch((error) => this.setState({ errorData: { ...getWrongTeamResponseInfo(error) } }));
|
||||
}
|
||||
|
||||
if (alertReceiveChannel || isNewAlertReceiveChannel) {
|
||||
this.setState({ alertReceiveChannelId: id });
|
||||
}
|
||||
};
|
||||
|
||||
update = () => {
|
||||
const { store } = this.props;
|
||||
const { page, integrationsFilters } = this.state;
|
||||
LocationHelper.update({ p: page }, 'partial');
|
||||
|
||||
return store.alertReceiveChannelStore.updateItems(integrationsFilters);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { store, query } = this.props;
|
||||
const { alertReceiveChannelId } = this.state;
|
||||
const { grafanaTeamStore, alertReceiveChannelStore, heartbeatStore, maintenanceStore } = store;
|
||||
|
||||
const results = alertReceiveChannelStore.getSearchResult();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
width: '25%',
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
render: this.renderName,
|
||||
},
|
||||
|
||||
{
|
||||
width: '15%',
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
render: (item: AlertReceiveChannel) => this.renderIntegrationStatus(item, alertReceiveChannelStore),
|
||||
},
|
||||
{
|
||||
width: '25%',
|
||||
title: 'Datasource',
|
||||
key: 'datasource',
|
||||
render: (item: AlertReceiveChannel) => this.renderDatasource(item, alertReceiveChannelStore),
|
||||
},
|
||||
{
|
||||
width: '10%',
|
||||
title: 'Maintenance',
|
||||
key: 'maintenance',
|
||||
render: (item: AlertReceiveChannel) => this.renderMaintenance(item, maintenanceStore, alertReceiveChannelStore),
|
||||
},
|
||||
{
|
||||
width: '5%',
|
||||
title: 'Heartbeat',
|
||||
key: 'heartbeat',
|
||||
render: (item: AlertReceiveChannel) => this.renderHeartbeat(item, alertReceiveChannelStore, heartbeatStore),
|
||||
},
|
||||
{
|
||||
width: '20%',
|
||||
title: 'Team',
|
||||
render: (item: AlertReceiveChannel) => this.renderTeam(item, grafanaTeamStore.items),
|
||||
},
|
||||
{
|
||||
width: '50px',
|
||||
key: 'buttons',
|
||||
render: (item: AlertReceiveChannel) => this.renderButtons(item),
|
||||
className: cx('buttons'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cx('root')}>
|
||||
<div className={cx('title')}>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.setState({ alertReceiveChannelId: 'new' });
|
||||
}}
|
||||
icon="plus"
|
||||
className={cx('newIntegrationButton')}
|
||||
>
|
||||
New integration
|
||||
</Button>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div>
|
||||
<RemoteFilters
|
||||
query={query}
|
||||
page="integrations"
|
||||
grafanaTeamStore={store.grafanaTeamStore}
|
||||
onChange={this.handleIntegrationsFiltersChange}
|
||||
/>
|
||||
<GTable
|
||||
emptyText={this.renderNotFound()}
|
||||
rowKey="id"
|
||||
data={results}
|
||||
columns={columns}
|
||||
// pagination={{
|
||||
// page,
|
||||
// total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
|
||||
// onChange: this.handleChangePage,
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{alertReceiveChannelId && (
|
||||
<IntegrationForm
|
||||
onHide={() => {
|
||||
this.setState({ alertReceiveChannelId: undefined });
|
||||
}}
|
||||
onUpdate={this.update}
|
||||
id={alertReceiveChannelId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
handleChangePage = (page: number) => {
|
||||
this.setState({ page }, this.update);
|
||||
};
|
||||
|
||||
renderNotFound() {
|
||||
return (
|
||||
<div className={cx('loader')}>
|
||||
<Text type="secondary">Not found</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderName(item: AlertReceiveChannel) {
|
||||
return (
|
||||
<PluginLink query={{ page: 'integrations_2', id: item.id }}>
|
||||
<Text type="link" size="medium">
|
||||
<Emoji className={cx('title')} text={item.verbal_name} />
|
||||
</Text>
|
||||
</PluginLink>
|
||||
);
|
||||
}
|
||||
|
||||
renderDatasource(item: AlertReceiveChannel, alertReceiveChannelStore) {
|
||||
const alertReceiveChannel = alertReceiveChannelStore.items[item.id];
|
||||
const integration = alertReceiveChannelStore.getIntegration(alertReceiveChannel);
|
||||
return (
|
||||
<HorizontalGroup spacing="xs">
|
||||
<IntegrationLogo scale={0.08} integration={integration} />
|
||||
<Text type="secondary" size="small">
|
||||
{integration?.display_name}
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderIntegrationStatus(item: AlertReceiveChannel, alertReceiveChannelStore) {
|
||||
const alertReceiveChannelCounter = alertReceiveChannelStore.counters[item.id];
|
||||
let routesCounter = undefined;
|
||||
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
{alertReceiveChannelCounter && (
|
||||
<PluginLink query={{ page: 'incidents', integration: item.id }} className={cx('alertsInfoText')}>
|
||||
<Badge
|
||||
text={alertReceiveChannelCounter?.alerts_count + '/' + alertReceiveChannelCounter?.alert_groups_count}
|
||||
color={'blue'}
|
||||
tooltip={
|
||||
alertReceiveChannelCounter?.alerts_count +
|
||||
' alert' +
|
||||
(alertReceiveChannelCounter?.alerts_count === 1 ? '' : 's') +
|
||||
' in ' +
|
||||
alertReceiveChannelCounter?.alert_groups_count +
|
||||
' alert group' +
|
||||
(alertReceiveChannelCounter?.alert_groups_count === 1 ? '' : 's')
|
||||
}
|
||||
/>
|
||||
</PluginLink>
|
||||
)}
|
||||
{routesCounter && <Badge text={routesCounter} color={'green'} tooltip={`${routesCounter} routes`} />}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeartbeat(item: AlertReceiveChannel, alertReceiveChannelStore, heartbeatStore) {
|
||||
const alertReceiveChannel = alertReceiveChannelStore.items[item.id];
|
||||
|
||||
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
|
||||
const heartbeat = heartbeatStore.items[heartbeatId];
|
||||
|
||||
const heartbeatStatus = Boolean(heartbeat?.status);
|
||||
return (
|
||||
<div className={cx('heartbeat')}>
|
||||
{alertReceiveChannel.is_available_for_integration_heartbeat && (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
content={
|
||||
heartbeat
|
||||
? `Last heartbeat: ${heartbeat.last_heartbeat_time_verbal || 'never'}`
|
||||
: 'Click to setup heartbeat'
|
||||
}
|
||||
>
|
||||
<div className={cx('heartbeat-icon')} onClick={() => {}}>
|
||||
{heartbeatStatus ? <HeartGreenIcon /> : <HeartRedIcon />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
convertTimestampToTimeDifference(timestamp: string) {
|
||||
const date = new Date(Number(timestamp) * 1000);
|
||||
const timezoneOffset = new Date().getTimezoneOffset() * 60;
|
||||
const localTimestamp = date.getTime() + timezoneOffset;
|
||||
const currentTime = Date.now();
|
||||
const difference = localTimestamp - currentTime;
|
||||
|
||||
let timeLeft;
|
||||
if (difference < 3600000) {
|
||||
timeLeft = Math.floor(difference / 60000) + 'm left';
|
||||
} else {
|
||||
timeLeft = Math.floor(difference / 3600000) + 'h left';
|
||||
}
|
||||
|
||||
return timeLeft;
|
||||
}
|
||||
|
||||
renderMaintenance(item: AlertReceiveChannel, maintenanceStore, alertReceiveChannelStore) {
|
||||
const maintenanceMode = item.maintenance_mode;
|
||||
const maintenanceTill = item.maintenance_till;
|
||||
|
||||
if (maintenanceMode === MaintenanceMode.Debug || maintenanceMode === MaintenanceMode.Maintenance) {
|
||||
return (
|
||||
<Tooltip placement="top" content="Stop maintenance mode">
|
||||
<Badge
|
||||
text={
|
||||
<Button
|
||||
className={cx('maintenance-button')}
|
||||
disabled={!isUserActionAllowed(UserActions.MaintenanceWrite)}
|
||||
fill="text"
|
||||
icon="pause"
|
||||
onClick={() => this.handleStopMaintenance(item, maintenanceStore, alertReceiveChannelStore)}
|
||||
>
|
||||
{this.convertTimestampToTimeDifference(maintenanceTill)}
|
||||
</Button>
|
||||
}
|
||||
color={maintenanceMode === MaintenanceMode.Debug ? 'orange' : 'blue'}
|
||||
tooltip={
|
||||
maintenanceMode === MaintenanceMode.Debug
|
||||
? `Debug Maintenance: ${this.convertTimestampToTimeDifference(maintenanceTill)} left`
|
||||
: `Maintenance: ${this.convertTimestampToTimeDifference(maintenanceTill)} left`
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
handleStopMaintenance = (item: AlertReceiveChannel, maintenanceStore, alertReceiveChannelStore) => {
|
||||
maintenanceStore.stopMaintenanceMode(MaintenanceType.alert_receive_channel, item.id).then(() => {
|
||||
alertReceiveChannelStore.updateItem(item.id);
|
||||
});
|
||||
};
|
||||
|
||||
renderTeam(item: AlertReceiveChannel, teams: any) {
|
||||
return <TeamName team={teams[item.team]} />;
|
||||
}
|
||||
|
||||
renderButtons = (item: AlertReceiveChannel) => {
|
||||
return (
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<WithPermissionControlTooltip key="edit" userAction={UserActions.IntegrationsWrite}>
|
||||
<IconButton tooltip="Settings" name="cog" onClick={() => this.onIntegrationEditClick(item.id)} />
|
||||
</WithPermissionControlTooltip>
|
||||
<WithPermissionControlTooltip key="edit" userAction={UserActions.IntegrationsWrite}>
|
||||
<WithConfirm>
|
||||
<IconButton
|
||||
tooltip="Delete"
|
||||
name="trash-alt"
|
||||
onClick={() => this.handleDeleteAlertReceiveChannel(item.id)}
|
||||
/>
|
||||
</WithConfirm>
|
||||
</WithPermissionControlTooltip>
|
||||
</HorizontalGroup>
|
||||
);
|
||||
};
|
||||
|
||||
onIntegrationEditClick = (id: AlertReceiveChannel['id']) => {
|
||||
this.setState({ alertReceiveChannelId: id });
|
||||
};
|
||||
|
||||
handleDeleteAlertReceiveChannel = (alertReceiveChannelId: AlertReceiveChannel['id']) => {
|
||||
const { store } = this.props;
|
||||
|
||||
const { alertReceiveChannelStore } = store;
|
||||
|
||||
alertReceiveChannelStore.deleteAlertReceiveChannel(alertReceiveChannelId).then(this.applyFilters);
|
||||
};
|
||||
|
||||
handleIntegrationsFiltersChange = (integrationsFilters: Filters) => {
|
||||
this.setState({ integrationsFilters }, () => this.debouncedUpdateIntegrations());
|
||||
};
|
||||
|
||||
applyFilters = () => {
|
||||
const { store } = this.props;
|
||||
const { alertReceiveChannelStore } = store;
|
||||
const { integrationsFilters } = this.state;
|
||||
|
||||
return alertReceiveChannelStore.updateItems(integrationsFilters);
|
||||
};
|
||||
|
||||
debouncedUpdateIntegrations = debounce(this.applyFilters, FILTERS_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
export default withRouter(withMobXProviderContext(Integrations));
|
||||
|
|
@ -14,6 +14,8 @@ import CloudPage from 'pages/settings/tabs/Cloud/CloudPage';
|
|||
import LiveSettingsPage from 'pages/settings/tabs/LiveSettings/LiveSettingsPage';
|
||||
import UsersPage from 'pages/users/Users';
|
||||
|
||||
import IntegrationsPage2 from './integrations_2/Integrations2';
|
||||
|
||||
export interface NavRoute {
|
||||
id: string;
|
||||
component: (props?: any) => JSX.Element;
|
||||
|
|
@ -36,6 +38,10 @@ export const routes: { [id: string]: NavRoute } = [
|
|||
component: IntegrationsPage,
|
||||
id: 'integrations',
|
||||
},
|
||||
{
|
||||
component: IntegrationsPage2,
|
||||
id: 'integrations_2',
|
||||
},
|
||||
{
|
||||
component: EscalationsChainsPage,
|
||||
id: 'escalations',
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import { observer } from 'mobx-react';
|
|||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import CounterBadge from 'components/CounterBadge/CounterBadge';
|
||||
import { MatchMediaTooltip } from 'components/MatchMediaTooltip/MatchMediaTooltip';
|
||||
import NewScheduleSelector from 'components/NewScheduleSelector/NewScheduleSelector';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import { SchedulesFiltersType } from 'components/SchedulesFilters/SchedulesFilters.types';
|
||||
import StatusCounterBadgeWithTooltip from 'components/StatusCounterBadgeWithTooltip/StatusCounterBadgeWithTooltip';
|
||||
import Table from 'components/Table/Table';
|
||||
import Text from 'components/Text/Text';
|
||||
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
|
||||
|
|
@ -306,8 +306,9 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
return (
|
||||
<HorizontalGroup>
|
||||
{item.number_of_escalation_chains > 0 && (
|
||||
<StatusCounterBadgeWithTooltip
|
||||
type="link"
|
||||
<CounterBadge
|
||||
borderType="link"
|
||||
icon="link"
|
||||
count={item.number_of_escalation_chains}
|
||||
tooltipTitle="Used in escalations"
|
||||
tooltipContent={
|
||||
|
|
@ -325,7 +326,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
'Not used yet'
|
||||
)
|
||||
) : (
|
||||
<LoadingPlaceholder>Loading related escalation chains....</LoadingPlaceholder>
|
||||
<LoadingPlaceholder text="Loading related escalation chains..." />
|
||||
)}
|
||||
</VerticalGroup>
|
||||
}
|
||||
|
|
@ -334,8 +335,9 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
)}
|
||||
|
||||
{item.warnings?.length > 0 && (
|
||||
<StatusCounterBadgeWithTooltip
|
||||
type="warning"
|
||||
<CounterBadge
|
||||
borderType="warning"
|
||||
icon="exclamation-triangle"
|
||||
count={item.warnings.length}
|
||||
tooltipTitle="Warnings"
|
||||
tooltipContent={
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import LegacyNavHeading from 'navbar/LegacyNavHeading';
|
|||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import Avatar from 'components/Avatar/Avatar';
|
||||
import CounterBadge from 'components/CounterBadge/CounterBadge';
|
||||
import GTable from 'components/GTable/GTable';
|
||||
import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper';
|
||||
import {
|
||||
|
|
@ -15,7 +16,6 @@ import {
|
|||
initErrorDataState,
|
||||
} from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import StatusCounterBadgeWithTooltip from 'components/StatusCounterBadgeWithTooltip/StatusCounterBadgeWithTooltip';
|
||||
import Text from 'components/Text/Text';
|
||||
import UsersFilters from 'components/UsersFilters/UsersFilters';
|
||||
import UserSettings from 'containers/UserSettings/UserSettings';
|
||||
|
|
@ -361,8 +361,9 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
|
||||
return (
|
||||
<HorizontalGroup>
|
||||
<StatusCounterBadgeWithTooltip
|
||||
type="warning"
|
||||
<CounterBadge
|
||||
borderType="warning"
|
||||
icon="exclamation-triangle"
|
||||
count={texts.length}
|
||||
tooltipTitle="Warnings"
|
||||
tooltipContent={
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ import NoMatch from 'pages/NoMatch';
|
|||
import EscalationChains from 'pages/escalation-chains/EscalationChains';
|
||||
import Incident from 'pages/incident/Incident';
|
||||
import Incidents from 'pages/incidents/Incidents';
|
||||
import Integration2 from 'pages/integration_2/Integration2';
|
||||
import Integrations from 'pages/integrations/Integrations';
|
||||
import Integrations2 from 'pages/integrations_2/Integrations2';
|
||||
import Maintenance from 'pages/maintenance/Maintenance';
|
||||
import OrganizationLogPage from 'pages/organization-logs/OrganizationLog';
|
||||
import OutgoingWebhooks from 'pages/outgoing_webhooks/OutgoingWebhooks';
|
||||
|
|
@ -151,6 +153,12 @@ export const Root = observer((props: AppRootProps) => {
|
|||
<Route path={getRoutesForPage('integrations')} exact>
|
||||
<Integrations query={query} />
|
||||
</Route>
|
||||
<Route path={getRoutesForPage('integrations_2')} exact>
|
||||
<Integrations2 query={query} />
|
||||
</Route>
|
||||
<Route path={getRoutesForPage('integration_2')} exact>
|
||||
<Integration2 query={query} />
|
||||
</Route>
|
||||
<Route path={getRoutesForPage('escalations')} exact>
|
||||
<EscalationChains query={query} />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -1,175 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PluginSetup app initialized with topnavbar = false 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="spin"
|
||||
>
|
||||
<img
|
||||
alt="Grafana OnCall Logo"
|
||||
src="[object Object]"
|
||||
/>
|
||||
<div
|
||||
class="spin-text"
|
||||
>
|
||||
Initializing plugin...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginSetup app initialized with topnavbar = true 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="spin"
|
||||
>
|
||||
<img
|
||||
alt="Grafana OnCall Logo"
|
||||
src="[object Object]"
|
||||
/>
|
||||
<div
|
||||
class="spin-text"
|
||||
>
|
||||
Initializing plugin...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginSetup app is loading 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="spin"
|
||||
>
|
||||
<img
|
||||
alt="Grafana OnCall Logo"
|
||||
src="[object Object]"
|
||||
/>
|
||||
<div
|
||||
class="spin-text"
|
||||
>
|
||||
Initializing plugin...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginSetup app successfully initialized 1`] = `
|
||||
<div>
|
||||
<div>
|
||||
hello
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginSetup there is an error message - retry setup 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="spin"
|
||||
>
|
||||
<img
|
||||
alt="Grafana OnCall Logo"
|
||||
src="[object Object]"
|
||||
/>
|
||||
<div
|
||||
class="spin-text"
|
||||
>
|
||||
ohhhh noo
|
||||
</div>
|
||||
<div
|
||||
class="configure-plugin"
|
||||
>
|
||||
<div
|
||||
class="css-ve64a7-horizontal-group"
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
<div
|
||||
class="css-cvef6c-layoutChildrenWrapper"
|
||||
>
|
||||
<button
|
||||
class="css-zy62io-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Retry
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="css-cvef6c-layoutChildrenWrapper"
|
||||
>
|
||||
<a
|
||||
class="css-zy62io-button"
|
||||
href="/plugins/grafana-oncall-app?page=configuration"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Configure Plugin
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PluginSetup there is an error message 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="spin"
|
||||
>
|
||||
<img
|
||||
alt="Grafana OnCall Logo"
|
||||
src="[object Object]"
|
||||
/>
|
||||
<div
|
||||
class="spin-text"
|
||||
>
|
||||
ohhhh noo
|
||||
</div>
|
||||
<div
|
||||
class="configure-plugin"
|
||||
>
|
||||
<div
|
||||
class="css-ve64a7-horizontal-group"
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
<div
|
||||
class="css-cvef6c-layoutChildrenWrapper"
|
||||
>
|
||||
<button
|
||||
class="css-zy62io-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Retry
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="css-cvef6c-layoutChildrenWrapper"
|
||||
>
|
||||
<a
|
||||
class="css-zy62io-button"
|
||||
href="/plugins/grafana-oncall-app?page=configuration"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="css-1mhnkuh"
|
||||
>
|
||||
Configure Plugin
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -61,3 +61,10 @@
|
|||
.u-cursor-default {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.thin-line-break {
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--always-gray);
|
||||
margin-top: 8px;
|
||||
opacity: 15%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,16 @@
|
|||
--tag-warning: #c69b06;
|
||||
--tag-primary: #299c46;
|
||||
--tag-secondary: #464c54;
|
||||
--tag-background-primary: rgba(56, 113, 220, 0.2);
|
||||
--tag-border-primary: rgba(56, 113, 220, 0.2);
|
||||
--tag-text-primary: rgba(110, 159, 255, 1);
|
||||
--tag-border-danger: rgb(151, 11, 27);
|
||||
--tag-text-danger: rgb(247, 144, 156);
|
||||
--tag-border-warning: rgb(150, 75, 0);
|
||||
--tag-background-warning: rgba(245, 183, 61, 0.18);
|
||||
--tag-text-warning: rgb(255, 190, 124);
|
||||
--tag-border-success: rgb(49, 100, 43);
|
||||
--tag-background-success: rgba(27, 133, 94, 0.15);
|
||||
--tag-text-success: rgb(165, 214, 159);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,3 +33,5 @@ export const DOCS_TELEGRAM_SETUP = 'https://grafana.com/docs/oncall/latest/chat-
|
|||
|
||||
// Make sure if you chage max-width here you also change it in responsive.css
|
||||
export const TABLE_COLUMN_MAX_WIDTH = 1500;
|
||||
|
||||
export const DATASOURCE_ALERTING = 'alertmanager';
|
||||
|
|
|
|||
|
|
@ -1155,6 +1155,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/runtime@^7.18.0":
|
||||
version "7.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
|
||||
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.3.3":
|
||||
version "7.18.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
|
||||
|
|
@ -1482,32 +1489,6 @@
|
|||
dependencies:
|
||||
tslib "2.4.0"
|
||||
|
||||
"@grafana/data@9.2.4", "@grafana/data@^9.2.4":
|
||||
version "9.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@grafana/data/-/data-9.2.4.tgz#38067f207006c07754c3ed5a8835dc1909df7e2d"
|
||||
integrity sha512-ukrvtQ0CzijpRZhBriv3LX935BKXRX4jf9l+jgK2uZJSYFAMbgz/Fvfagfr7sYmIPe8Ms4r3hslu2hbynWHzTw==
|
||||
dependencies:
|
||||
"@braintree/sanitize-url" "6.0.0"
|
||||
"@grafana/schema" "9.2.4"
|
||||
"@types/d3-interpolate" "^1.4.0"
|
||||
d3-interpolate "1.4.0"
|
||||
date-fns "2.29.1"
|
||||
eventemitter3 "4.0.7"
|
||||
fast_array_intersect "1.1.0"
|
||||
history "4.10.1"
|
||||
lodash "4.17.21"
|
||||
marked "4.1.0"
|
||||
moment "2.29.4"
|
||||
moment-timezone "0.5.35"
|
||||
ol "6.15.1"
|
||||
papaparse "5.3.2"
|
||||
regenerator-runtime "0.13.9"
|
||||
rxjs "7.5.6"
|
||||
tinycolor2 "1.4.2"
|
||||
tslib "2.4.0"
|
||||
uplot "1.6.22"
|
||||
xss "1.0.13"
|
||||
|
||||
"@grafana/data@9.2.6":
|
||||
version "9.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@grafana/data/-/data-9.2.6.tgz#a8b108fe882a16349e903013e62cb6c741f82135"
|
||||
|
|
@ -1560,14 +1541,58 @@
|
|||
uplot "1.6.22"
|
||||
xss "1.0.14"
|
||||
|
||||
"@grafana/e2e-selectors@9.2.4":
|
||||
version "9.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@grafana/e2e-selectors/-/e2e-selectors-9.2.4.tgz#748539cc0313ee1c23055a100313235ef2fca64b"
|
||||
integrity sha512-k8Pqjb5yZa/rT0djUNceiDQCN6SIpYciwJbfn/8fl5zAEMLpInx9n8EfnefkinaAfxKcMB4IhDH/R+l4D0hAlQ==
|
||||
"@grafana/data@9.4.7":
|
||||
version "9.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@grafana/data/-/data-9.4.7.tgz#8b4c15a5b52ec13908c006baf87416354ee8251a"
|
||||
integrity sha512-GnP91XSuTlRaT4crRh7OgC58rKsF/ANAZTFeHOYqVD7r47upTgnnnM46khSLhvA3MoKfNZflXOneaIjU4c5Hyw==
|
||||
dependencies:
|
||||
"@grafana/tsconfig" "^1.2.0-rc1"
|
||||
"@braintree/sanitize-url" "6.0.1"
|
||||
"@grafana/schema" "9.4.7"
|
||||
"@types/d3-interpolate" "^3.0.0"
|
||||
d3-interpolate "3.0.1"
|
||||
date-fns "2.29.3"
|
||||
eventemitter3 "4.0.7"
|
||||
fast_array_intersect "1.1.0"
|
||||
history "4.10.1"
|
||||
lodash "4.17.21"
|
||||
marked "4.2.0"
|
||||
moment "2.29.4"
|
||||
moment-timezone "0.5.38"
|
||||
ol "7.1.0"
|
||||
papaparse "5.3.2"
|
||||
react-use "17.4.0"
|
||||
regenerator-runtime "0.13.10"
|
||||
rxjs "7.5.7"
|
||||
tinycolor2 "1.4.2"
|
||||
tslib "2.4.1"
|
||||
uplot "1.6.24"
|
||||
xss "1.0.14"
|
||||
|
||||
"@grafana/data@^9.2.4":
|
||||
version "9.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@grafana/data/-/data-9.2.4.tgz#38067f207006c07754c3ed5a8835dc1909df7e2d"
|
||||
integrity sha512-ukrvtQ0CzijpRZhBriv3LX935BKXRX4jf9l+jgK2uZJSYFAMbgz/Fvfagfr7sYmIPe8Ms4r3hslu2hbynWHzTw==
|
||||
dependencies:
|
||||
"@braintree/sanitize-url" "6.0.0"
|
||||
"@grafana/schema" "9.2.4"
|
||||
"@types/d3-interpolate" "^1.4.0"
|
||||
d3-interpolate "1.4.0"
|
||||
date-fns "2.29.1"
|
||||
eventemitter3 "4.0.7"
|
||||
fast_array_intersect "1.1.0"
|
||||
history "4.10.1"
|
||||
lodash "4.17.21"
|
||||
marked "4.1.0"
|
||||
moment "2.29.4"
|
||||
moment-timezone "0.5.35"
|
||||
ol "6.15.1"
|
||||
papaparse "5.3.2"
|
||||
regenerator-runtime "0.13.9"
|
||||
rxjs "7.5.6"
|
||||
tinycolor2 "1.4.2"
|
||||
tslib "2.4.0"
|
||||
typescript "4.8.2"
|
||||
uplot "1.6.22"
|
||||
xss "1.0.13"
|
||||
|
||||
"@grafana/e2e-selectors@9.2.6":
|
||||
version "9.2.6"
|
||||
|
|
@ -1587,6 +1612,15 @@
|
|||
tslib "2.4.1"
|
||||
typescript "4.8.4"
|
||||
|
||||
"@grafana/e2e-selectors@9.4.7":
|
||||
version "9.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@grafana/e2e-selectors/-/e2e-selectors-9.4.7.tgz#7632bf927dc885ddeea0a865084badf93b2d777a"
|
||||
integrity sha512-HvLgA9gccMC1uPx5Q+858yPjkfD5O0Kekm0p/ufQn+BA8dFbPpqVVd5cnu+/J3duKKHOsGBvZIShIOKNzkYw8g==
|
||||
dependencies:
|
||||
"@grafana/tsconfig" "^1.2.0-rc1"
|
||||
tslib "2.4.1"
|
||||
typescript "4.8.4"
|
||||
|
||||
"@grafana/eslint-config@5.0.0", "@grafana/eslint-config@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@grafana/eslint-config/-/eslint-config-5.0.0.tgz#e08a89d378772340bc6cd1872ec4d15666269aba"
|
||||
|
|
@ -1696,6 +1730,13 @@
|
|||
dependencies:
|
||||
tslib "2.4.1"
|
||||
|
||||
"@grafana/schema@9.4.7":
|
||||
version "9.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@grafana/schema/-/schema-9.4.7.tgz#bb918ec7f096e0b81d7ead921ac1addeb265dd0e"
|
||||
integrity sha512-uTrg/XmMhfxXTSRskNRdUzDCK9XdwHHnNJkfUltzSF5v16bc9iE1u/NrkuEBxoLh6hji9Gd6pw7mS0K9o9/0ww==
|
||||
dependencies:
|
||||
tslib "2.4.1"
|
||||
|
||||
"@grafana/toolkit@^9.2.4":
|
||||
version "9.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@grafana/toolkit/-/toolkit-9.2.6.tgz#55d424321a65a027f3365c6e0df649bcc1d2c9d6"
|
||||
|
|
@ -1927,18 +1968,19 @@
|
|||
uplot "1.6.22"
|
||||
uuid "9.0.0"
|
||||
|
||||
"@grafana/ui@^9.2.4":
|
||||
version "9.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-9.2.4.tgz#885b0f10bd700aa0dc094f2fcb554477fc47e410"
|
||||
integrity sha512-V9sNQwcAkMAmWjM/DLMw9X+J0nqBmrwNV1uJ1kyS+3cRRwCNyJsZUz3NuOnzCbvCEl3bopLyY/WBSHONbLEoig==
|
||||
"@grafana/ui@^9.4.7":
|
||||
version "9.4.7"
|
||||
resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-9.4.7.tgz#19ed1b36db85013070da118f4d87f13abb38567c"
|
||||
integrity sha512-MnEXrGRh3t4LkShP/Q0bfzFooiE4xbDagQ/17/B1VIwMWECsYeSQsEYuA2p/9yjTpOiL2YfB72uyAThpGYpQew==
|
||||
dependencies:
|
||||
"@emotion/css" "11.9.0"
|
||||
"@emotion/react" "11.9.3"
|
||||
"@grafana/data" "9.2.4"
|
||||
"@grafana/e2e-selectors" "9.2.4"
|
||||
"@grafana/schema" "9.2.4"
|
||||
"@monaco-editor/react" "4.4.5"
|
||||
"@popperjs/core" "2.11.5"
|
||||
"@emotion/css" "11.10.5"
|
||||
"@emotion/react" "11.10.5"
|
||||
"@grafana/data" "9.4.7"
|
||||
"@grafana/e2e-selectors" "9.4.7"
|
||||
"@grafana/schema" "9.4.7"
|
||||
"@leeoniya/ufuzzy" "0.9.0"
|
||||
"@monaco-editor/react" "4.4.6"
|
||||
"@popperjs/core" "2.11.6"
|
||||
"@react-aria/button" "3.6.1"
|
||||
"@react-aria/dialog" "3.3.1"
|
||||
"@react-aria/focus" "3.8.0"
|
||||
|
|
@ -1949,49 +1991,52 @@
|
|||
"@sentry/browser" "6.19.7"
|
||||
ansicolor "1.1.100"
|
||||
calculate-size "1.1.1"
|
||||
classnames "2.3.1"
|
||||
core-js "3.25.1"
|
||||
d3 "5.15.0"
|
||||
date-fns "2.29.1"
|
||||
classnames "2.3.2"
|
||||
core-js "3.27.1"
|
||||
d3 "7.8.2"
|
||||
date-fns "2.29.3"
|
||||
hoist-non-react-statics "3.3.2"
|
||||
immutable "4.1.0"
|
||||
i18next "^22.0.0"
|
||||
immutable "4.2.2"
|
||||
is-hotkey "0.2.0"
|
||||
jquery "3.6.0"
|
||||
jquery "3.6.1"
|
||||
lodash "4.17.21"
|
||||
memoize-one "6.0.0"
|
||||
moment "2.29.4"
|
||||
monaco-editor "0.34.0"
|
||||
ol "6.15.1"
|
||||
ol "7.1.0"
|
||||
prismjs "1.29.0"
|
||||
rc-cascader "3.6.1"
|
||||
rc-drawer "4.4.3"
|
||||
rc-slider "9.7.5"
|
||||
rc-cascader "3.8.0"
|
||||
rc-drawer "6.1.2"
|
||||
rc-slider "10.1.0"
|
||||
rc-time-picker "^3.7.3"
|
||||
react-beautiful-dnd "13.1.0"
|
||||
react-calendar "3.7.0"
|
||||
react-colorful "5.5.1"
|
||||
rc-tooltip "5.3.1"
|
||||
react-beautiful-dnd "13.1.1"
|
||||
react-calendar "3.9.0"
|
||||
react-colorful "5.6.1"
|
||||
react-custom-scrollbars-2 "4.5.0"
|
||||
react-dropzone "14.2.2"
|
||||
react-highlight-words "0.18.0"
|
||||
react-dropzone "14.2.3"
|
||||
react-highlight-words "0.20.0"
|
||||
react-hook-form "7.5.3"
|
||||
react-inlinesvg "3.0.0"
|
||||
react-i18next "^12.0.0"
|
||||
react-inlinesvg "3.0.1"
|
||||
react-popper "2.3.0"
|
||||
react-popper-tooltip "^4.3.1"
|
||||
react-popper-tooltip "4.4.2"
|
||||
react-router-dom "^5.2.0"
|
||||
react-select "5.4.0"
|
||||
react-select "5.6.0"
|
||||
react-select-event "^5.1.0"
|
||||
react-table "7.8.0"
|
||||
react-transition-group "4.4.2"
|
||||
react-transition-group "4.4.5"
|
||||
react-use "17.4.0"
|
||||
react-window "1.8.7"
|
||||
rxjs "7.5.6"
|
||||
react-window "1.8.8"
|
||||
rxjs "7.5.7"
|
||||
slate "0.47.9"
|
||||
slate-plain-serializer "0.7.11"
|
||||
slate-plain-serializer "0.7.13"
|
||||
slate-react "0.22.10"
|
||||
tinycolor2 "1.4.2"
|
||||
tslib "2.4.0"
|
||||
uplot "1.6.22"
|
||||
uuid "8.3.2"
|
||||
tslib "2.4.1"
|
||||
uplot "1.6.24"
|
||||
uuid "9.0.0"
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.6":
|
||||
version "0.11.7"
|
||||
|
|
@ -2326,6 +2371,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@leeoniya/ufuzzy/-/ufuzzy-0.8.0.tgz#2ccfc29453e168ce5866bf6dee89771db404a7f7"
|
||||
integrity sha512-EOc0fEsIqe6CDZxC14efhybnPcXyJi7VaZby40mWASZD0CI78ONoF+4+LGlcT58jsAIwEims5ARbRqo+BVHEAQ==
|
||||
|
||||
"@leeoniya/ufuzzy@0.9.0":
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@leeoniya/ufuzzy/-/ufuzzy-0.9.0.tgz#efb8f19f64ef6ff754fc49935c9ad53ab99712c1"
|
||||
integrity sha512-p2zWsX0GwO1x723Yhb3KLAoSwp1geQvzRPHgIoOR/0qn8Ptpsb3b01+W47iAYR/NWo0pX36XQoTU0alVRykMAg==
|
||||
|
||||
"@mapbox/jsonlint-lines-primitives@~2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234"
|
||||
|
|
@ -2628,6 +2678,15 @@
|
|||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
|
||||
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
|
||||
|
||||
"@rc-component/portal@^1.0.0-6":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/portal/-/portal-1.1.1.tgz#1a30ffe51c240b54360cba8e8bfc5d1f559325c4"
|
||||
integrity sha512-m8w3dFXX0H6UkJ4wtfrSwhe2/6M08uz24HHrF8pWfAXPwA9hwCuTE5per/C86KwNLouRpwFGcr7LfpHaa1F38g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.0"
|
||||
classnames "^2.3.2"
|
||||
rc-util "^5.24.4"
|
||||
|
||||
"@react-aria/button@3.6.1":
|
||||
version "3.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/button/-/button-3.6.1.tgz#111e296df8e171e4eb227c306f087337490bc896"
|
||||
|
|
@ -3135,6 +3194,11 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/d3-color@*":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.0.tgz#6594da178ded6c7c3842f3cc0ac84b156f12f2d4"
|
||||
integrity sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==
|
||||
|
||||
"@types/d3-color@^1":
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.4.2.tgz#944f281d04a0f06e134ea96adbb68303515b2784"
|
||||
|
|
@ -3147,6 +3211,13 @@
|
|||
dependencies:
|
||||
"@types/d3-color" "^1"
|
||||
|
||||
"@types/d3-interpolate@^3.0.0":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc"
|
||||
integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==
|
||||
dependencies:
|
||||
"@types/d3-color" "*"
|
||||
|
||||
"@types/dompurify@^2.3.4":
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.3.4.tgz#94e997e30338ea24d4c8d08beca91ce4dd17a1b4"
|
||||
|
|
@ -4962,7 +5033,7 @@ classnames@2.3.1:
|
|||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||
|
||||
classnames@2.3.2, classnames@2.x, classnames@^2.2.1, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1:
|
||||
classnames@2.3.2, classnames@2.x, classnames@^2.2.1, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
|
||||
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
|
||||
|
|
@ -5106,16 +5177,16 @@ commander@2, commander@^2.20.0, commander@^2.20.3:
|
|||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
|
||||
commander@7, commander@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||
|
||||
commander@^6.2.0:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
|
||||
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
|
||||
|
||||
commander@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||
|
||||
commander@^8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
|
||||
|
|
@ -5252,6 +5323,11 @@ core-js@3.26.0:
|
|||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.26.0.tgz#a516db0ed0811be10eac5d94f3b8463d03faccfe"
|
||||
integrity sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==
|
||||
|
||||
core-js@3.27.1:
|
||||
version "3.27.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.27.1.tgz#23cc909b315a6bb4e418bf40a52758af2103ba46"
|
||||
integrity sha512-GutwJLBChfGCpwwhbYoqfv03LAfmiz7e7D/BNxzeMxwQf10GRSzqiOjx7AmtEk+heiD/JWmBuyBPgFtx0Sg1ww==
|
||||
|
||||
core-js@^2.4.0:
|
||||
version "2.6.12"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
|
||||
|
|
@ -5526,11 +5602,23 @@ d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
|
||||
integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
|
||||
|
||||
"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.3.tgz#39f1f4954e4a09ff69ac597c2d61906b04e84740"
|
||||
integrity sha512-JRHwbQQ84XuAESWhvIPaUV4/1UYTBOLiOPGWqgFDHZS1D5QN9c57FbH3QpEnQMYiOXNzKUQyGTZf+EVO7RT5TQ==
|
||||
dependencies:
|
||||
internmap "1 - 2"
|
||||
|
||||
d3-axis@1:
|
||||
version "1.0.12"
|
||||
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9"
|
||||
integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==
|
||||
|
||||
d3-axis@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322"
|
||||
integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
|
||||
|
||||
d3-brush@1:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.1.6.tgz#b0a22c7372cabec128bdddf9bddc058592f89e9b"
|
||||
|
|
@ -5542,6 +5630,17 @@ d3-brush@1:
|
|||
d3-selection "1"
|
||||
d3-transition "1"
|
||||
|
||||
d3-brush@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
|
||||
integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-drag "2 - 3"
|
||||
d3-interpolate "1 - 3"
|
||||
d3-selection "3"
|
||||
d3-transition "3"
|
||||
|
||||
d3-chord@1:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.6.tgz#309157e3f2db2c752f0280fedd35f2067ccbb15f"
|
||||
|
|
@ -5550,6 +5649,13 @@ d3-chord@1:
|
|||
d3-array "1"
|
||||
d3-path "1"
|
||||
|
||||
d3-chord@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966"
|
||||
integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
|
||||
dependencies:
|
||||
d3-path "1 - 3"
|
||||
|
||||
d3-collection@1:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
|
||||
|
|
@ -5560,6 +5666,11 @@ d3-color@1:
|
|||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a"
|
||||
integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==
|
||||
|
||||
"d3-color@1 - 3", d3-color@3:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
|
||||
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
|
||||
|
||||
d3-contour@1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3"
|
||||
|
|
@ -5567,11 +5678,30 @@ d3-contour@1:
|
|||
dependencies:
|
||||
d3-array "^1.1.1"
|
||||
|
||||
d3-contour@4:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc"
|
||||
integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==
|
||||
dependencies:
|
||||
d3-array "^3.2.0"
|
||||
|
||||
d3-delaunay@6:
|
||||
version "6.0.4"
|
||||
resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b"
|
||||
integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==
|
||||
dependencies:
|
||||
delaunator "5"
|
||||
|
||||
d3-dispatch@1:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58"
|
||||
integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==
|
||||
|
||||
"d3-dispatch@1 - 3", d3-dispatch@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
|
||||
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
|
||||
|
||||
d3-drag@1:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.5.tgz#2537f451acd39d31406677b7dc77c82f7d988f70"
|
||||
|
|
@ -5580,6 +5710,14 @@ d3-drag@1:
|
|||
d3-dispatch "1"
|
||||
d3-selection "1"
|
||||
|
||||
"d3-drag@2 - 3", d3-drag@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
|
||||
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-selection "3"
|
||||
|
||||
d3-dsv@1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.2.0.tgz#9d5f75c3a5f8abd611f74d3f5847b0d4338b885c"
|
||||
|
|
@ -5589,11 +5727,25 @@ d3-dsv@1:
|
|||
iconv-lite "0.4"
|
||||
rw "1"
|
||||
|
||||
"d3-dsv@1 - 3", d3-dsv@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
|
||||
integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
|
||||
dependencies:
|
||||
commander "7"
|
||||
iconv-lite "0.6"
|
||||
rw "1"
|
||||
|
||||
d3-ease@1:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2"
|
||||
integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==
|
||||
|
||||
"d3-ease@1 - 3", d3-ease@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
|
||||
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
|
||||
|
||||
d3-fetch@1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.2.0.tgz#15ce2ecfc41b092b1db50abd2c552c2316cf7fc7"
|
||||
|
|
@ -5601,6 +5753,13 @@ d3-fetch@1:
|
|||
dependencies:
|
||||
d3-dsv "1"
|
||||
|
||||
d3-fetch@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22"
|
||||
integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
|
||||
dependencies:
|
||||
d3-dsv "1 - 3"
|
||||
|
||||
d3-force@1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.1.tgz#fd29a5d1ff181c9e7f0669e4bd72bdb0e914ec0b"
|
||||
|
|
@ -5611,11 +5770,25 @@ d3-force@1:
|
|||
d3-quadtree "1"
|
||||
d3-timer "1"
|
||||
|
||||
d3-force@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
|
||||
integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-quadtree "1 - 3"
|
||||
d3-timer "1 - 3"
|
||||
|
||||
d3-format@1:
|
||||
version "1.4.5"
|
||||
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4"
|
||||
integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==
|
||||
|
||||
"d3-format@1 - 3", d3-format@3:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
|
||||
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
|
||||
|
||||
d3-geo@1:
|
||||
version "1.12.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.12.1.tgz#7fc2ab7414b72e59fbcbd603e80d9adc029b035f"
|
||||
|
|
@ -5623,11 +5796,23 @@ d3-geo@1:
|
|||
dependencies:
|
||||
d3-array "1"
|
||||
|
||||
d3-geo@3:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e"
|
||||
integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==
|
||||
dependencies:
|
||||
d3-array "2.5.0 - 3"
|
||||
|
||||
d3-hierarchy@1:
|
||||
version "1.1.9"
|
||||
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83"
|
||||
integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==
|
||||
|
||||
d3-hierarchy@3:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
|
||||
integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
|
||||
|
||||
d3-interpolate@1, d3-interpolate@1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
|
||||
|
|
@ -5635,26 +5820,53 @@ d3-interpolate@1, d3-interpolate@1.4.0:
|
|||
dependencies:
|
||||
d3-color "1"
|
||||
|
||||
"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3, d3-interpolate@3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
|
||||
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
|
||||
dependencies:
|
||||
d3-color "1 - 3"
|
||||
|
||||
d3-path@1:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
|
||||
integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
|
||||
|
||||
"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
|
||||
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
|
||||
|
||||
d3-polygon@1:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e"
|
||||
integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==
|
||||
|
||||
d3-polygon@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398"
|
||||
integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
|
||||
|
||||
d3-quadtree@1:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz#ca8b84df7bb53763fe3c2f24bd435137f4e53135"
|
||||
integrity sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==
|
||||
|
||||
"d3-quadtree@1 - 3", d3-quadtree@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
|
||||
integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
|
||||
|
||||
d3-random@1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291"
|
||||
integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==
|
||||
|
||||
d3-random@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4"
|
||||
integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
|
||||
|
||||
d3-scale-chromatic@1:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98"
|
||||
|
|
@ -5663,6 +5875,14 @@ d3-scale-chromatic@1:
|
|||
d3-color "1"
|
||||
d3-interpolate "1"
|
||||
|
||||
d3-scale-chromatic@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a"
|
||||
integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==
|
||||
dependencies:
|
||||
d3-color "1 - 3"
|
||||
d3-interpolate "1 - 3"
|
||||
|
||||
d3-scale@2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f"
|
||||
|
|
@ -5675,11 +5895,27 @@ d3-scale@2:
|
|||
d3-time "1"
|
||||
d3-time-format "2"
|
||||
|
||||
d3-scale@4:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
|
||||
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
|
||||
dependencies:
|
||||
d3-array "2.10.0 - 3"
|
||||
d3-format "1 - 3"
|
||||
d3-interpolate "1.2.0 - 3"
|
||||
d3-time "2.1.1 - 3"
|
||||
d3-time-format "2 - 4"
|
||||
|
||||
d3-selection@1, d3-selection@^1.1.0:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c"
|
||||
integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==
|
||||
|
||||
"d3-selection@2 - 3", d3-selection@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
|
||||
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
|
||||
|
||||
d3-shape@1:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
|
||||
|
|
@ -5687,6 +5923,13 @@ d3-shape@1:
|
|||
dependencies:
|
||||
d3-path "1"
|
||||
|
||||
d3-shape@3:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
|
||||
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
|
||||
dependencies:
|
||||
d3-path "^3.1.0"
|
||||
|
||||
d3-time-format@2:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850"
|
||||
|
|
@ -5694,16 +5937,35 @@ d3-time-format@2:
|
|||
dependencies:
|
||||
d3-time "1"
|
||||
|
||||
"d3-time-format@2 - 4", d3-time-format@4:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
|
||||
integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
|
||||
dependencies:
|
||||
d3-time "1 - 3"
|
||||
|
||||
d3-time@1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
|
||||
integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
|
||||
|
||||
"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
|
||||
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
|
||||
dependencies:
|
||||
d3-array "2 - 3"
|
||||
|
||||
d3-timer@1:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5"
|
||||
integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==
|
||||
|
||||
"d3-timer@1 - 3", d3-timer@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
|
||||
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
|
||||
|
||||
d3-transition@1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398"
|
||||
|
|
@ -5716,6 +5978,17 @@ d3-transition@1:
|
|||
d3-selection "^1.1.0"
|
||||
d3-timer "1"
|
||||
|
||||
"d3-transition@2 - 3", d3-transition@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
|
||||
integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
|
||||
dependencies:
|
||||
d3-color "1 - 3"
|
||||
d3-dispatch "1 - 3"
|
||||
d3-ease "1 - 3"
|
||||
d3-interpolate "1 - 3"
|
||||
d3-timer "1 - 3"
|
||||
|
||||
d3-voronoi@1:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297"
|
||||
|
|
@ -5732,6 +6005,17 @@ d3-zoom@1:
|
|||
d3-selection "1"
|
||||
d3-transition "1"
|
||||
|
||||
d3-zoom@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
|
||||
integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-drag "2 - 3"
|
||||
d3-interpolate "1 - 3"
|
||||
d3-selection "2 - 3"
|
||||
d3-transition "2 - 3"
|
||||
|
||||
d3@5.15.0:
|
||||
version "5.15.0"
|
||||
resolved "https://registry.yarnpkg.com/d3/-/d3-5.15.0.tgz#ffd44958e6a3cb8a59a84429c45429b8bca5677a"
|
||||
|
|
@ -5769,6 +6053,42 @@ d3@5.15.0:
|
|||
d3-voronoi "1"
|
||||
d3-zoom "1"
|
||||
|
||||
d3@7.8.2:
|
||||
version "7.8.2"
|
||||
resolved "https://registry.yarnpkg.com/d3/-/d3-7.8.2.tgz#2bdb3c178d095ae03b107a18837ae049838e372d"
|
||||
integrity sha512-WXty7qOGSHb7HR7CfOzwN1Gw04MUOzN8qh9ZUsvwycIMb4DYMpY9xczZ6jUorGtO6bR9BPMPaueIKwiDxu9uiQ==
|
||||
dependencies:
|
||||
d3-array "3"
|
||||
d3-axis "3"
|
||||
d3-brush "3"
|
||||
d3-chord "3"
|
||||
d3-color "3"
|
||||
d3-contour "4"
|
||||
d3-delaunay "6"
|
||||
d3-dispatch "3"
|
||||
d3-drag "3"
|
||||
d3-dsv "3"
|
||||
d3-ease "3"
|
||||
d3-fetch "3"
|
||||
d3-force "3"
|
||||
d3-format "3"
|
||||
d3-geo "3"
|
||||
d3-hierarchy "3"
|
||||
d3-interpolate "3"
|
||||
d3-path "3"
|
||||
d3-polygon "3"
|
||||
d3-quadtree "3"
|
||||
d3-random "3"
|
||||
d3-scale "4"
|
||||
d3-scale-chromatic "3"
|
||||
d3-selection "3"
|
||||
d3-shape "3"
|
||||
d3-time "3"
|
||||
d3-time-format "4"
|
||||
d3-timer "3"
|
||||
d3-transition "3"
|
||||
d3-zoom "3"
|
||||
|
||||
data-urls@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
|
||||
|
|
@ -5929,6 +6249,13 @@ del@^5.1.0:
|
|||
rimraf "^3.0.0"
|
||||
slash "^3.0.0"
|
||||
|
||||
delaunator@5:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b"
|
||||
integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==
|
||||
dependencies:
|
||||
robust-predicates "^3.0.0"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
|
|
@ -7662,7 +7989,7 @@ iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24:
|
|||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
iconv-lite@^0.6.3:
|
||||
iconv-lite@0.6, iconv-lite@^0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
||||
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
|
||||
|
|
@ -7699,6 +8026,11 @@ immutable@4.1.0, immutable@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
|
||||
integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
|
||||
|
||||
immutable@4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.2.tgz#2da9ff4384a4330c36d4d1bc88e90f9e0b0ccd16"
|
||||
integrity sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==
|
||||
|
||||
import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
|
|
@ -7804,6 +8136,11 @@ internal-slot@^1.0.3:
|
|||
has "^1.0.3"
|
||||
side-channel "^1.0.4"
|
||||
|
||||
"internmap@1 - 2":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
|
||||
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
|
||||
|
||||
interpret@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
|
||||
|
|
@ -11200,6 +11537,18 @@ rc-cascader@3.7.0:
|
|||
rc-tree "~5.7.0"
|
||||
rc-util "^5.6.1"
|
||||
|
||||
rc-cascader@3.8.0:
|
||||
version "3.8.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.8.0.tgz#5eaca8998b2e3f5692d13f16bfe2346eccc87c6a"
|
||||
integrity sha512-zCz/NzsNRQ1TIfiR3rQNxjeRvgRHEkNdo0FjHQZ6Ay6n4tdCmMrM7+81ThNaf21JLQ1gS2AUG2t5uikGV78obA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
array-tree-filter "^2.1.0"
|
||||
classnames "^2.3.1"
|
||||
rc-select "~14.2.0"
|
||||
rc-tree "~5.7.0"
|
||||
rc-util "^5.6.1"
|
||||
|
||||
rc-drawer@4.4.3:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-4.4.3.tgz#2094937a844e55dc9644236a2d9fba79c344e321"
|
||||
|
|
@ -11209,6 +11558,17 @@ rc-drawer@4.4.3:
|
|||
classnames "^2.2.6"
|
||||
rc-util "^5.7.0"
|
||||
|
||||
rc-drawer@6.1.2:
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-6.1.2.tgz#032918a21bfa8a7d9e52ada1e7b8ed08c0ae6346"
|
||||
integrity sha512-mYsTVT8Amy0LRrpVEv7gI1hOjtfMSO/qHAaCDzFx9QBLnms3cAQLJkaxRWM+Eq99oyLhU/JkgoqTg13bc4ogOQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
"@rc-component/portal" "^1.0.0-6"
|
||||
classnames "^2.2.6"
|
||||
rc-motion "^2.6.1"
|
||||
rc-util "^5.21.2"
|
||||
|
||||
rc-motion@^2.0.0, rc-motion@^2.0.1:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-2.6.2.tgz#3d31f97e41fb8e4f91a4a4189b6a98ac63342869"
|
||||
|
|
@ -11218,6 +11578,15 @@ rc-motion@^2.0.0, rc-motion@^2.0.1:
|
|||
classnames "^2.2.1"
|
||||
rc-util "^5.21.0"
|
||||
|
||||
rc-motion@^2.6.1:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-2.6.3.tgz#e6d8ca06591c2c1bcd3391a8e7a822ebc4d94e9c"
|
||||
integrity sha512-xFLkes3/7VL/J+ah9jJruEW/Akbx5F6jVa2wG5o/ApGKQKSOd5FR3rseHLL9+xtJg4PmCwo6/1tqhDO/T+jFHA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.1"
|
||||
classnames "^2.2.1"
|
||||
rc-util "^5.21.0"
|
||||
|
||||
rc-overflow@^1.0.0:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rc-overflow/-/rc-overflow-1.2.8.tgz#40f140fabc244118543e627cdd1ef750d9481a88"
|
||||
|
|
@ -11251,6 +11620,19 @@ rc-select@~14.1.0:
|
|||
rc-util "^5.16.1"
|
||||
rc-virtual-list "^3.2.0"
|
||||
|
||||
rc-select@~14.2.0:
|
||||
version "14.2.2"
|
||||
resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.2.2.tgz#03558848b190d24fc9010a3bf1104c6dbea9b122"
|
||||
integrity sha512-w+LuiYGFWgaV23PuxtdeWtXSsoxt+eCfzxu/CvRuqSRm8tn/pqvAb1xUIDAjoMMWK1FqiOW4jI/iMt7ZRG/BBg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "2.x"
|
||||
rc-motion "^2.0.1"
|
||||
rc-overflow "^1.0.0"
|
||||
rc-trigger "^5.0.4"
|
||||
rc-util "^5.16.1"
|
||||
rc-virtual-list "^3.4.13"
|
||||
|
||||
rc-slider@10.0.1:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-10.0.1.tgz#7058c68ff1e1aa4e7c3536e5e10128bdbccb87f9"
|
||||
|
|
@ -11261,6 +11643,16 @@ rc-slider@10.0.1:
|
|||
rc-util "^5.18.1"
|
||||
shallowequal "^1.1.0"
|
||||
|
||||
rc-slider@10.1.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-10.1.0.tgz#11e401d8412ae20f9c2ee478bdbaddd042158753"
|
||||
integrity sha512-nhC8V0+lNj4gGKZix2QAfcj/EP3NvCtFhNJPFMvXUdn7pe8bSa2vXNSxQVN5b9veVSic4Xeqgd/7KamX3gqznA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.5"
|
||||
rc-util "^5.18.1"
|
||||
shallowequal "^1.1.0"
|
||||
|
||||
rc-slider@9.7.5:
|
||||
version "9.7.5"
|
||||
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-9.7.5.tgz#193141c68e99b1dc3b746daeb6bf852946f5b7f4"
|
||||
|
|
@ -11304,6 +11696,15 @@ rc-tooltip@5.2.2, rc-tooltip@^5.0.1:
|
|||
classnames "^2.3.1"
|
||||
rc-trigger "^5.0.0"
|
||||
|
||||
rc-tooltip@5.3.1:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-5.3.1.tgz#3dde4e1865f79cd23f202bba4e585c2a1173024b"
|
||||
integrity sha512-e6H0dMD38EPaSPD2XC8dRfct27VvT2TkPdoBSuNl3RRZ5tspiY/c5xYEmGC0IrABvMBgque4Mr2SMZuliCvoiQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.2"
|
||||
classnames "^2.3.1"
|
||||
rc-trigger "^5.3.1"
|
||||
|
||||
rc-tree@~5.6.3:
|
||||
version "5.6.9"
|
||||
resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-5.6.9.tgz#b73290a6dcad65e4ed5d8dc21cb198b30316404b"
|
||||
|
|
@ -11350,6 +11751,17 @@ rc-trigger@^5.0.0, rc-trigger@^5.0.4:
|
|||
rc-motion "^2.0.0"
|
||||
rc-util "^5.19.2"
|
||||
|
||||
rc-trigger@^5.3.1:
|
||||
version "5.3.4"
|
||||
resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.3.4.tgz#6b4b26e32825677c837d1eb4d7085035eecf9a61"
|
||||
integrity sha512-mQv+vas0TwKcjAO2izNPkqR4j86OemLRmvL2nOzdP9OWNWA1ivoTt5hzFqYNW9zACwmTezRiN8bttrC7cZzYSw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.3"
|
||||
classnames "^2.2.6"
|
||||
rc-align "^4.0.0"
|
||||
rc-motion "^2.0.0"
|
||||
rc-util "^5.19.2"
|
||||
|
||||
rc-util@^4.0.4, rc-util@^4.15.3, rc-util@^4.4.0:
|
||||
version "4.21.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.21.1.tgz#88602d0c3185020aa1053d9a1e70eac161becb05"
|
||||
|
|
@ -11370,6 +11782,14 @@ rc-util@^5.15.0, rc-util@^5.16.1, rc-util@^5.18.1, rc-util@^5.19.2, rc-util@^5.2
|
|||
react-is "^16.12.0"
|
||||
shallowequal "^1.1.0"
|
||||
|
||||
rc-util@^5.21.2, rc-util@^5.24.4:
|
||||
version "5.29.3"
|
||||
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.29.3.tgz#dc02b7b2103468e9fdf14e0daa58584f47898e37"
|
||||
integrity sha512-wX6ZwQTzY2v7phJBquN4mSEIFR0E0qumlENx0zjENtDvoVSq2s7cR95UidKRO1hOHfDsecsfM9D1gO4Kebs7fA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.3"
|
||||
react-is "^16.12.0"
|
||||
|
||||
rc-virtual-list@^3.2.0, rc-virtual-list@^3.4.8:
|
||||
version "3.4.11"
|
||||
resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.4.11.tgz#97f5e947380d546a2ca8ad229d8e41e9b33b20c6"
|
||||
|
|
@ -11380,6 +11800,16 @@ rc-virtual-list@^3.2.0, rc-virtual-list@^3.4.8:
|
|||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.15.0"
|
||||
|
||||
rc-virtual-list@^3.4.13:
|
||||
version "3.4.13"
|
||||
resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.4.13.tgz#20acc934b263abcf7b7c161f50ef82281b2f7e8d"
|
||||
integrity sha512-cPOVDmcNM7rH6ANotanMDilW/55XnFPw0Jh/GQYtrzZSy3AmWvCnqVNyNC/pgg3lfVmX2994dlzAhuUrd4jG7w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.0"
|
||||
classnames "^2.2.6"
|
||||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.15.0"
|
||||
|
||||
react-beautiful-dnd@13.1.0:
|
||||
version "13.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d"
|
||||
|
|
@ -11553,6 +11983,15 @@ react-highlight-words@0.18.0:
|
|||
memoize-one "^4.0.0"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-highlight-words@0.20.0:
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.20.0.tgz#c60bfff5d14678c8f0e8fbe4bdcf083e6c70d507"
|
||||
integrity sha512-asCxy+jCehDVhusNmCBoxDf2mm1AJ//D+EzDx1m5K7EqsMBIHdZ5G4LdwbSEXqZq1Ros0G0UySWmAtntSph7XA==
|
||||
dependencies:
|
||||
highlight-words-core "^1.2.0"
|
||||
memoize-one "^4.0.0"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-hook-form@7.5.3:
|
||||
version "7.5.3"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.5.3.tgz#9a624fa14ec153b154891c5ebddae02ec5c2e40f"
|
||||
|
|
@ -11619,7 +12058,7 @@ react-modal@^3.15.1:
|
|||
react-lifecycles-compat "^3.0.0"
|
||||
warning "^4.0.3"
|
||||
|
||||
react-popper-tooltip@^4.3.1:
|
||||
react-popper-tooltip@4.4.2, react-popper-tooltip@^4.3.1:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/react-popper-tooltip/-/react-popper-tooltip-4.4.2.tgz#0dc4894b8e00ba731f89bd2d30584f6032ec6163"
|
||||
integrity sha512-y48r0mpzysRTZAIh8m2kpZ8S1YPNqGtQPDrlXYSGvDS1c1GpG/NUXbsbIdfbhXfmSaRJuTcaT6N1q3CKuHRVbg==
|
||||
|
|
@ -12206,6 +12645,11 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
|
|||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
robust-predicates@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a"
|
||||
integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==
|
||||
|
||||
rtl-css-js@^1.14.0:
|
||||
version "1.16.0"
|
||||
resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.16.0.tgz#e8d682982441aadb63cabcb2f7385f3fb78ff26e"
|
||||
|
|
@ -13644,6 +14088,11 @@ uplot@1.6.22:
|
|||
resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.22.tgz#28a136c7c5fce92ce5e25f38f19314a029bec390"
|
||||
integrity sha512-2jtSb/YHUgtmIUn0+QJjf7ggcJicb5PKe7ijBiRDTPsG/f8F/MFayZ+g6/0kATNkDyF/qQsHJDmCp6cxncg1EQ==
|
||||
|
||||
uplot@1.6.24:
|
||||
version "1.6.24"
|
||||
resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.24.tgz#dfa213fa7da92763261920ea972ed1a5f9f6af12"
|
||||
integrity sha512-WpH2BsrFrqxkMu+4XBvc0eCDsRBhzoq9crttYeSI0bfxpzR5YoSVzZXOKFVWcVC7sp/aDXrdDPbDZGCtck2PVg==
|
||||
|
||||
upper-case-first@^1.1.0, upper-case-first@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue