diff --git a/playwright/snapshots/TriggerInfo/triggerinfo--default.png b/playwright/snapshots/TriggerInfo/triggerinfo--default.png index a2f585f1e..9cb37aa4c 100644 Binary files a/playwright/snapshots/TriggerInfo/triggerinfo--default.png and b/playwright/snapshots/TriggerInfo/triggerinfo--default.png differ diff --git a/playwright/snapshots/TriggerInfo/triggerinfo--description-in-multiple-line.png b/playwright/snapshots/TriggerInfo/triggerinfo--description-in-multiple-line.png index 1a9342a91..8c029a553 100644 Binary files a/playwright/snapshots/TriggerInfo/triggerinfo--description-in-multiple-line.png and b/playwright/snapshots/TriggerInfo/triggerinfo--description-in-multiple-line.png differ diff --git a/playwright/snapshots/TriggerInfo/triggerinfo--not-everyday.png b/playwright/snapshots/TriggerInfo/triggerinfo--not-everyday.png index 8247438f9..38c291586 100644 Binary files a/playwright/snapshots/TriggerInfo/triggerinfo--not-everyday.png and b/playwright/snapshots/TriggerInfo/triggerinfo--not-everyday.png differ diff --git a/playwright/snapshots/TriggerInfo/triggerinfo--with-maintenance-and-maintenance-info.png b/playwright/snapshots/TriggerInfo/triggerinfo--with-maintenance-and-maintenance-info.png index 9df882876..aaa0fde32 100644 Binary files a/playwright/snapshots/TriggerInfo/triggerinfo--with-maintenance-and-maintenance-info.png and b/playwright/snapshots/TriggerInfo/triggerinfo--with-maintenance-and-maintenance-info.png differ diff --git a/playwright/snapshots/TriggerInfo/triggerinfo--with-maintenance.png b/playwright/snapshots/TriggerInfo/triggerinfo--with-maintenance.png index 1be7770a4..aaa0fde32 100644 Binary files a/playwright/snapshots/TriggerInfo/triggerinfo--with-maintenance.png and b/playwright/snapshots/TriggerInfo/triggerinfo--with-maintenance.png differ diff --git a/playwright/snapshots/TriggerInfo/triggerinfo--with-throttling.png b/playwright/snapshots/TriggerInfo/triggerinfo--with-throttling.png index e33d7c5bc..9cb37aa4c 100644 Binary files a/playwright/snapshots/TriggerInfo/triggerinfo--with-throttling.png and b/playwright/snapshots/TriggerInfo/triggerinfo--with-throttling.png differ diff --git a/playwright/snapshots/TriggerInfo/triggerinfo--witherror.png b/playwright/snapshots/TriggerInfo/triggerinfo--witherror.png index c4f4532d2..87a8c423e 100644 Binary files a/playwright/snapshots/TriggerInfo/triggerinfo--witherror.png and b/playwright/snapshots/TriggerInfo/triggerinfo--witherror.png differ diff --git a/src/Components/ConfirmDeleteModal/ConfirmDeleteModal.tsx b/src/Components/ConfirmDeleteModal/ConfirmDeleteModal.tsx index 7138efe05..440813d45 100644 --- a/src/Components/ConfirmDeleteModal/ConfirmDeleteModal.tsx +++ b/src/Components/ConfirmDeleteModal/ConfirmDeleteModal.tsx @@ -5,7 +5,7 @@ interface ConfirmDeleteModalProps { message: string; children?: React.ReactNode; onClose: () => void; - onDelete?: () => Promise; + onDelete?: () => void; } export const ConfirmDeleteModal = (props: ConfirmDeleteModalProps): JSX.Element => ( diff --git a/src/Components/ContactEditModal/ContactEditModal.tsx b/src/Components/ContactEditModal/ContactEditModal.tsx index 12f819fed..2cc0944aa 100644 --- a/src/Components/ContactEditModal/ContactEditModal.tsx +++ b/src/Components/ContactEditModal/ContactEditModal.tsx @@ -71,6 +71,7 @@ export default class ContactEditModal extends React.Component { Save and test diff --git a/src/Components/FileExport/FileExport.tsx b/src/Components/FileExport/FileExport.tsx index 7a0c342f3..c0d72c2cf 100644 --- a/src/Components/FileExport/FileExport.tsx +++ b/src/Components/FileExport/FileExport.tsx @@ -10,18 +10,26 @@ type FileExportProps = { title: string; data: Partial; children?: React.ReactNode; + isButton?: boolean; }; -export default function FileExport({ title, data, children }: FileExportProps): React.ReactElement { +export default function FileExport({ + title, + data, + children, + isButton, +}: FileExportProps): React.ReactElement { const handleExport = () => { const fileData = JSON.stringify(data, undefined, 4); const blob = new Blob([fileData], { type: "application/json" }); saveAs(blob, `${title}.json`); }; - return ( + return isButton ? ( + ) : ( + {children || "Export"} ); } diff --git a/src/Components/SubscriptionEditModal/SubscriptionEditModal.tsx b/src/Components/SubscriptionEditModal/SubscriptionEditModal.tsx index 6ead8c05a..26db1af9e 100644 --- a/src/Components/SubscriptionEditModal/SubscriptionEditModal.tsx +++ b/src/Components/SubscriptionEditModal/SubscriptionEditModal.tsx @@ -79,6 +79,7 @@ export default class SubscriptionEditModal extends React.Component Save and test diff --git a/src/Components/TriggerInfo/Components/LinkMenuItem.tsx b/src/Components/TriggerInfo/Components/LinkMenuItem.tsx new file mode 100644 index 000000000..adf63753a --- /dev/null +++ b/src/Components/TriggerInfo/Components/LinkMenuItem.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { Link } from "@skbkontur/react-ui/components/Link"; +import { MenuItem } from "@skbkontur/react-ui"; + +interface ILinkMenuItemProps { + link?: string; + icon?: React.ReactElement; + onClick?: () => void; + children: React.ReactElement | string; +} + +export const LinkMenuItem = ({ link, icon, onClick, children }: ILinkMenuItemProps) => { + return ( + { + return ( + + ); + }} + > + {children} + + ); +}; diff --git a/src/Components/TriggerInfo/Components/ScheduleView.tsx b/src/Components/TriggerInfo/Components/ScheduleView.tsx new file mode 100644 index 000000000..f9736a101 --- /dev/null +++ b/src/Components/TriggerInfo/Components/ScheduleView.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Schedule } from "../../../Domain/Schedule"; +import { format, addMinutes, startOfDay } from "date-fns"; +import { getUTCDate } from "../../../helpers/DateUtil"; + +export function ScheduleView(props: { data: Schedule }): React.ReactElement { + const { data } = props; + const { days, startOffset, endOffset, tzOffset } = data; + + const startTime = format(addMinutes(startOfDay(getUTCDate()), startOffset), "HH:mm"); + + const endTime = format(addMinutes(startOfDay(getUTCDate()), endOffset), "HH:mm"); + + const timeZone = format(addMinutes(startOfDay(getUTCDate()), Math.abs(tzOffset)), "HH:mm"); + + const timeZoneSign = tzOffset < 0 ? "+" : "−"; + const enabledDays = days.filter(({ enabled }) => enabled); + + return ( + <> + {days.length === enabledDays.length + ? "Everyday" + : enabledDays.map(({ name }) => name).join(", ")}{" "} + {startTime}—{endTime} (GMT {tzOffset !== 0 && timeZoneSign} + {timeZone}) + + ); +} diff --git a/src/Components/TriggerInfo/TriggerInfo.tsx b/src/Components/TriggerInfo/TriggerInfo.tsx index df8c8fc15..a334a2e46 100644 --- a/src/Components/TriggerInfo/TriggerInfo.tsx +++ b/src/Components/TriggerInfo/TriggerInfo.tsx @@ -1,31 +1,41 @@ import * as React from "react"; import { History } from "history"; -import { format, addMinutes, startOfDay, fromUnixTime, getUnixTime } from "date-fns"; +import { format, fromUnixTime } from "date-fns"; import queryString from "query-string"; import { Link } from "@skbkontur/react-ui/components/Link"; import { Button } from "@skbkontur/react-ui/components/Button"; import { Tooltip } from "@skbkontur/react-ui/components/Tooltip"; +import ExportIcon from "@skbkontur/react-icons/Export"; +import TrashIcon from "@skbkontur/react-icons/Trash"; import ErrorIcon from "@skbkontur/react-icons/Error"; -import ClockIcon from "@skbkontur/react-icons/Clock"; import EditIcon from "@skbkontur/react-icons/Edit"; import ClearIcon from "@skbkontur/react-icons/Clear"; +import ClockIcon from "@skbkontur/react-icons/Clock"; +import ArrowTriangleDownIcon from "@skbkontur/react-icons/ArrowTriangleDown"; import DocumentCopyIcon from "@skbkontur/react-icons/DocumentCopy"; -import UserIcon from "@skbkontur/react-icons/User"; import TagGroup from "../TagGroup/TagGroup"; -import { Trigger, TriggerSource, TriggerState } from "../../Domain/Trigger"; -import { Schedule } from "../../Domain/Schedule"; +import { + Trigger, + TriggerState, + maintenanceDelta, + triggerSourceDescription, +} from "../../Domain/Trigger"; import { getPageLink } from "../../Domain/Global"; -import { getUTCDate, humanizeDuration } from "../../helpers/DateUtil"; +import { humanizeDuration } from "../../helpers/DateUtil"; import { omitTrigger } from "../../helpers/omitTypes"; import RouterLink from "../RouterLink/RouterLink"; import FileExport from "../FileExport/FileExport"; import MaintenanceSelect from "../MaintenanceSelect/MaintenanceSelect"; import { CodeEditor } from "../HighlightInput/CodeEditor"; -import { Gapped, Hint } from "@skbkontur/react-ui"; +import { Gapped, Hint, DropdownMenu, MenuSeparator } from "@skbkontur/react-ui"; import { CopyButton } from "../TriggerEditForm/Components/CopyButton"; import { Markdown } from "../Markdown/Markdown"; import { MetricStateChart } from "../MetricStateChart/MetricStateChart"; import { MetricItemList } from "../../Domain/Metric"; +import { ConfirmDeleteModal } from "../ConfirmDeleteModal/ConfirmDeleteModal"; +import { useModal } from "../../hooks/useModal"; +import { LinkMenuItem } from "./Components/LinkMenuItem"; +import { ScheduleView } from "./Components/ScheduleView"; import classNames from "classnames/bind"; import styles from "./TriggerInfo.less"; @@ -33,19 +43,16 @@ import styles from "./TriggerInfo.less"; const cn = classNames.bind(styles); interface IProps { - data: Trigger; + trigger: Trigger; triggerState: TriggerState; supportEmail?: string; metrics?: MetricItemList; + deleteTrigger: (id: string) => void; onThrottlingRemove: (triggerId: string) => void; onSetMaintenance: (maintenance: number) => void; history: History; } -function maintenanceDelta(maintenance?: number | null): number { - return (maintenance || 0) - getUnixTime(getUTCDate()); -} - function maintenanceCaption(delta: number): React.ReactNode { return ( @@ -56,46 +63,12 @@ function maintenanceCaption(delta: number): React.ReactNode { ); } -function ScheduleView(props: { data: Schedule }): React.ReactElement { - const { data } = props; - const { days, startOffset, endOffset, tzOffset } = data; - - const startTime = format(addMinutes(startOfDay(getUTCDate()), startOffset), "HH:mm"); - - const endTime = format(addMinutes(startOfDay(getUTCDate()), endOffset), "HH:mm"); - - const timeZone = format(addMinutes(startOfDay(getUTCDate()), Math.abs(tzOffset)), "HH:mm"); - - const timeZoneSign = tzOffset < 0 ? "+" : "−"; - const enabledDays = days.filter(({ enabled }) => enabled); - - return ( - <> - {days.length === enabledDays.length - ? "Everyday" - : enabledDays.map(({ name }) => name).join(", ")}{" "} - {startTime}—{endTime} (GMT {timeZoneSign} - {timeZone}) - - ); -} - -function triggerSourceDescription(source: TriggerSource): string | undefined { - switch (source) { - case TriggerSource.GRAPHITE_REMOTE: - return "(remote)"; - case TriggerSource.PROMETHEUS_REMOTE: - return "(prometheus)"; - case TriggerSource.GRAPHITE_LOCAL: - return undefined; - } -} - export default function TriggerInfo({ - data, + trigger, triggerState, supportEmail, metrics, + deleteTrigger, onThrottlingRemove, onSetMaintenance, history, @@ -114,14 +87,20 @@ export default function TriggerInfo({ tags, throttling, trigger_source: triggerSource, - } = data; - const { state, msg: exceptionMessage, maintenance, maintenanceInfo } = triggerState; + } = trigger; + const { state, msg: exceptionMessage, maintenance, maintenance_info } = triggerState; + const { isModalOpen, openModal, closeModal } = useModal(); const isMetrics = metrics && Object.keys(metrics).length > 0; const hasExpression = expression != null && expression !== ""; const hasMultipleTargets = targets.length > 1; const delta = maintenanceDelta(maintenance); + const onDeleteTrigger = () => { + closeModal(); + deleteTrigger(trigger.id); + }; + return (
@@ -129,64 +108,81 @@ export default function TriggerInfo({ {name != null && name !== "" ? name : "[No name]"}
- {throttling !== 0 && ( - - - - )} - - }> + + } + > Edit - + { + const isInfo = + delta > 0 && + maintenance_info && + maintenance_info.setup_user && + maintenance_info.setup_time; + + if (!isInfo) { + return null; + } + return ( +
+ Maintenance was set +
+ by {maintenance_info.setup_user} +
+ at{" "} + {format( + fromUnixTime(maintenance_info.setup_time), + "MMMM d, HH:mm:ss" + )} +
+ ); + }} + > + +
- - + Other + + } + > + {throttling !== 0 && ( + onThrottlingRemove(id)} + icon={} + > + Disable throttling + + )} + }> + + + } + link={getPageLink("triggerDuplicate", id)} > Duplicate - - - - - - - {delta > 0 && - maintenanceInfo && - maintenanceInfo.setup_user && - maintenanceInfo.setup_time && ( - ( -
- Maintenance was set -
- by {maintenanceInfo.setup_user} -
- at{" "} - {format( - fromUnixTime(maintenanceInfo.setup_time), - "MMMM d, HH:mm:ss" - )} -
- )} - > - -
- )} -
+ + + } onClick={openModal}> + Delete + +
@@ -273,7 +269,7 @@ export default function TriggerInfo({ Please verify trigger target {hasMultipleTargets ? "s" : ""} {hasExpression ? " and expression" : ""} on{" "} - + trigger edit page . @@ -300,6 +296,15 @@ export default function TriggerInfo({
)} + {isModalOpen && trigger && ( + + Trigger {trigger.name} will be deleted. + + )}
); } diff --git a/src/Containers/TriggerEditContainer.tsx b/src/Containers/TriggerEditContainer.tsx index 683cdd71d..1fbd4faa7 100644 --- a/src/Containers/TriggerEditContainer.tsx +++ b/src/Containers/TriggerEditContainer.tsx @@ -2,7 +2,6 @@ import React, { useState, useRef, useEffect } from "react"; import { RouteComponentProps } from "react-router"; import { ValidationContainer } from "@skbkontur/react-ui-validations"; import { Button } from "@skbkontur/react-ui"; -import TrashIcon from "@skbkontur/react-icons/Trash"; import { useSaveTrigger } from "../hooks/useSaveTrigger"; import MoiraApi from "../Api/MoiraApi"; import { withMoiraApi } from "../Api/MoiraApiInjection"; @@ -12,7 +11,6 @@ import { Config } from "../Domain/Config"; import RouterLink from "../Components/RouterLink/RouterLink"; import { Layout, LayoutContent, LayoutTitle } from "../Components/Layout/Layout"; import TriggerEditForm from "../Components/TriggerEditForm/TriggerEditForm"; -import { ConfirmDeleteModal } from "../Components/ConfirmDeleteModal/ConfirmDeleteModal"; import { ColumnStack, RowStack, Fit } from "../Components/ItemsStack/ItemsStack"; import { setError, @@ -23,13 +21,11 @@ import { } from "../hooks/useTriggerFormContainerReducer"; import { useValidateTrigger } from "../hooks/useValidateTrigger"; import { TriggerSaveWarningModal } from "../Components/TriggerSaveWarningModal/TriggerSaveWarningModal"; -import { useModal } from "../hooks/useModal"; type Props = RouteComponentProps<{ id?: string }> & { moiraApi: MoiraApi }; const TriggerEditContainer = (props: Props) => { const [state, dispatch] = useTriggerFormContainerReducer(); - const { isModalOpen, closeModal, openModal } = useModal(); const [trigger, setTrigger] = useState(undefined); const [tags, setTags] = useState(undefined); const [config, setConfig] = useState(undefined); @@ -60,18 +56,6 @@ const TriggerEditContainer = (props: Props) => { } }; - const deleteTrigger = async (id: string) => { - closeModal(); - dispatch(setIsLoading(true)); - try { - await props.moiraApi.delTrigger(id); - props.history.push(getPageLink("index")); - } catch (error) { - dispatch(setError(error.message)); - dispatch(setIsLoading(false)); - } - }; - const getData = async () => { if (typeof props.match.params.id !== "string") { dispatch(setError("Wrong trigger id")); @@ -139,26 +123,6 @@ const TriggerEditContainer = (props: Props) => { Save trigger - - {isModalOpen && ( - deleteTrigger(trigger.id)} - > - Trigger {trigger.name} will be - deleted. - - )} - - Cancel diff --git a/src/Domain/Trigger.ts b/src/Domain/Trigger.ts index 5b2994be3..bb9aa11e2 100644 --- a/src/Domain/Trigger.ts +++ b/src/Domain/Trigger.ts @@ -1,6 +1,8 @@ import { Status } from "./Status"; import { MetricItemList } from "./Metric"; import { Schedule } from "./Schedule"; +import { getUnixTime } from "date-fns"; +import { getUTCDate } from "../helpers/DateUtil"; export type TriggerType = "rising" | "falling" | "expression"; export const DEFAULT_TRIGGER_TYPE = "rising"; @@ -53,7 +55,7 @@ export type TriggerList = { export type TriggerState = { maintenance?: number; - maintenanceInfo?: { + maintenance_info?: { setup_user?: string | null; setup_time: number; }; @@ -136,4 +138,19 @@ export const checkTriggerTarget = ( } }; +export function triggerSourceDescription(source: TriggerSource): string | undefined { + switch (source) { + case TriggerSource.GRAPHITE_REMOTE: + return "(remote)"; + case TriggerSource.PROMETHEUS_REMOTE: + return "(prometheus)"; + case TriggerSource.GRAPHITE_LOCAL: + return undefined; + } +} + +export function maintenanceDelta(maintenance?: number | null): number { + return (maintenance || 0) - getUnixTime(getUTCDate()); +} + export default TriggerSource; diff --git a/src/Stories/TriggerInfo.stories.tsx b/src/Stories/TriggerInfo.stories.tsx index 9ebb188a1..d134ab2a6 100644 --- a/src/Stories/TriggerInfo.stories.tsx +++ b/src/Stories/TriggerInfo.stories.tsx @@ -137,7 +137,7 @@ const stories: Array<{ triggerState: { ...triggerState, maintenance: Date.now() / 1000 + 3600, - maintenanceInfo: { + maintenance_info: { setup_user: "Batman", setup_time: 1553158221, }, @@ -153,7 +153,8 @@ stories.forEach(({ title, data, triggerState: state }) => { { } const { dsn, platform } = config; - const isLocalPlatform = platform !== undefined; + const isLocalPlatform = platform === undefined; Sentry.init({ dsn, debug: isLocalPlatform, diff --git a/src/pages/trigger/trigger.desktop.tsx b/src/pages/trigger/trigger.desktop.tsx index ac5f64d0c..12ba99504 100644 --- a/src/pages/trigger/trigger.desktop.tsx +++ b/src/pages/trigger/trigger.desktop.tsx @@ -19,6 +19,7 @@ export type TriggerDesktopProps = { events: Array; page: number; pageCount: number; + deleteTrigger: (id: string) => void; disableThrottling: (id: string) => void; setTriggerMaintenance: (id: string, maintenance: number) => void; setMetricMaintenance: (id: string, metric: string, maintenance: number) => void; @@ -73,6 +74,7 @@ class TriggerDesktop extends React.Component { events, page, pageCount, + deleteTrigger, disableThrottling, setTriggerMaintenance, setMetricMaintenance, @@ -97,7 +99,7 @@ class TriggerDesktop extends React.Component { {trigger && state && ( disableThrottling(trigger.id)} onSetMaintenance={(maintenance) => @@ -105,6 +107,7 @@ class TriggerDesktop extends React.Component { } history={this.props.history} metrics={metrics} + deleteTrigger={deleteTrigger} /> )} diff --git a/src/pages/trigger/trigger.tsx b/src/pages/trigger/trigger.tsx index 1cea414c8..97dc90392 100644 --- a/src/pages/trigger/trigger.tsx +++ b/src/pages/trigger/trigger.tsx @@ -11,6 +11,7 @@ import { withMoiraApi } from "../../Api/MoiraApiInjection"; import transformPageFromHumanToProgrammer from "../../logic/transformPageFromHumanToProgrammer"; import { TriggerDesktopProps } from "./trigger.desktop"; import { TriggerMobileProps } from "./trigger.mobile"; +import { getPageLink } from "../../Domain/Global"; export type TriggerProps = RouteComponentProps<{ id: string }> & { moiraApi: MoiraApi; @@ -64,30 +65,6 @@ class TriggerPage extends React.Component { }; } - static getEventMetricName(event: Event, triggerName: string): string { - if (event.trigger_event) { - return triggerName; - } - return event.metric.length !== 0 ? event.metric : "No metric evaluated"; - } - - static composeEvents( - events: Array, - triggerName: string - ): { - [key: string]: Array; - } { - return events.reduce((data: { [key: string]: Array }, event: Event) => { - const metric = this.getEventMetricName(event, triggerName); - if (data[metric]) { - data[metric].push(event); - } else { - data[metric] = [event]; - } - return data; - }, {}); - } - render() { const { loading, @@ -109,6 +86,7 @@ class TriggerPage extends React.Component { pageCount={pageCount} loading={loading} error={error} + deleteTrigger={this.deleteTrigger} disableThrottling={this.disableThrottling} setTriggerMaintenance={this.setTriggerMaintenance} setMetricMaintenance={this.setMetricMaintenance} @@ -188,6 +166,16 @@ class TriggerPage extends React.Component { this.loadData(); }; + deleteTrigger = async (id: string) => { + const { moiraApi } = this.props; + try { + await moiraApi.delTrigger(id); + this.props.history.push(getPageLink("index")); + } catch (error) { + this.setState({ loading: false, error: error.message }); + } + }; + changeLocationSearch = (update: { page: number }) => { const { location, history } = this.props; const locationSearch = TriggerPage.parseLocationSearch(location.search);