diff --git a/playwright/e2eTests/triggerOperations.spec.ts b/playwright/e2eTests/triggerOperations.spec.ts index 4036a5f20..0ebefe729 100644 --- a/playwright/e2eTests/triggerOperations.spec.ts +++ b/playwright/e2eTests/triggerOperations.spec.ts @@ -2,6 +2,14 @@ import { test as base, expect } from "@playwright/test"; import { TriggerInfoPage } from "../pages/triggerInfo.page"; import { TriggerForm } from "../pages/triggerForm"; import { MainPage } from "../pages/main.page"; +import { + calculateMaintenanceTime, + getMaintenanceCaption, + Maintenance, + MaintenanceList, +} from "../../src/Domain/Maintenance"; +import { humanizeDuration } from "../../src/helpers/DateUtil"; +import { maintenanceDelta } from "../../src/Domain/Trigger"; const triggerName = "test trigger name"; const testTriggerDescription = "test trigger description"; @@ -48,7 +56,39 @@ test("Add trigger", async ({ triggerName, triggerDescription, page }) => { await expect(page).toHaveURL(`/trigger/${responseJson.id}`); await expect(page.getByText(triggerName)).toBeVisible(); await expect(page.getByText(triggerDescription)).toBeVisible(); - await page.waitForTimeout(1000); +}); + +test("Set trigger maintenance for all intervals", async ({ triggerName, page }) => { + const mainPage = new MainPage(page); + await mainPage.gotoMainPage(); + await page.getByText(triggerName).click(); + + const triggerInfoPage = new TriggerInfoPage(page); + + for (const maintenance of MaintenanceList) { + await test.step(`Set maintenance to ${getMaintenanceCaption(maintenance)}`, async () => { + const setMaintenanceRequestPromise = page.waitForRequest("**/trigger/*/setMaintenance"); + + const expectedTriggerTime = calculateMaintenanceTime(maintenance); + + await triggerInfoPage.triggerMaintenance.click(); + + await page.getByText(getMaintenanceCaption(maintenance)).click(); + + const setMaintenanceRequest = await setMaintenanceRequestPromise; + + const requestBody = JSON.parse(setMaintenanceRequest.postData() || "{}"); + expect(requestBody).not.toBeNull(); + expect(requestBody.trigger).toEqual(expectedTriggerTime); + + if (maintenance === Maintenance.off) { + await expect(page.getByText("Maintenance")).toBeVisible(); + } else + await expect( + page.getByText(humanizeDuration(maintenanceDelta(expectedTriggerTime))) + ).toBeVisible(); + }); + } }); test("Duplicate trigger", async ({ diff --git a/playwright/pages/triggerInfo.page.ts b/playwright/pages/triggerInfo.page.ts index e28c6af7f..3bbaf2d1d 100644 --- a/playwright/pages/triggerInfo.page.ts +++ b/playwright/pages/triggerInfo.page.ts @@ -8,6 +8,7 @@ export class TriggerInfoPage { readonly menuListButton: Locator; readonly duplicateButton: Locator; readonly deleteButton: Locator; + readonly triggerMaintenance: Locator; constructor(page: Page) { this.page = page; @@ -16,5 +17,6 @@ export class TriggerInfoPage { this.menuListButton = page.getByText("Other"); this.duplicateButton = page.getByText("Duplicate"); this.deleteButton = page.getByText("Delete"); + this.triggerMaintenance = page.locator("[data-tid='TriggerMaintenanceButton']"); } } diff --git a/src/Api/MoiraApi.ts b/src/Api/MoiraApi.ts deleted file mode 100644 index 0d2ea3db6..000000000 --- a/src/Api/MoiraApi.ts +++ /dev/null @@ -1,563 +0,0 @@ -import * as queryString from "query-string"; -import { EventList } from "../Domain/Event"; -import { Trigger, TriggerList, TriggerState, ValidateTargetsResult } from "../Domain/Trigger"; -import { Settings } from "../Domain/Settings"; -import { TagStat } from "../Domain/Tag"; -import { PatternList } from "../Domain/Pattern"; -import { NotificationList } from "../Domain/Notification"; -import { Contact, ContactList } from "../Domain/Contact"; -import { ContactCreateInfo } from "../Domain/ContactCreateInfo"; -import { Subscription } from "../Domain/Subscription"; -import { Schedule } from "../Domain/Schedule"; -import { NotifierState } from "../Domain/MoiraServiceStates"; -import { Team } from "../Domain/Team"; - -export type SubscriptionCreateInfo = { - sched: Schedule; - tags: Array<string>; - throttling: boolean; - contacts: Array<string>; - enabled: boolean; - any_tags: boolean; - user?: string; - team_id?: string; - id?: string; - ignore_recoverings: boolean; - ignore_warnings: boolean; - plotting?: { - enabled: boolean; - theme: "light" | "dark"; - }; -}; - -export type TagList = { - list: Array<string>; -}; - -export type TagStatList = { - list: Array<TagStat>; -}; - -export class ApiError extends Error { - status: number; - - constructor({ message, status }: { message: string; status: number }) { - super(message); - this.name = "ApiError"; - this.status = status; - } -} - -const statusCode = { - NOT_FOUND: 404, -}; - -export { statusCode }; - -export default class MoiraApi { - apiUrl: string; - - triggerListPageSize = 20; - - eventHistoryPageSize = 100; - - constructor(apiUrl: string) { - this.apiUrl = apiUrl; - } - - static async checkStatus(response: Response): Promise<void> { - if (!(response.status >= 200 && response.status < 300)) { - const serverResponse = await response.json(); - - throw new ApiError({ - message: serverResponse - ? serverResponse.status + - (serverResponse.error ? `: ${serverResponse.error}` : "") - : serverResponse.error, - status: response.status, - }); - } - } - - async get<T>(url: string, init?: RequestInit): Promise<T> { - const fullUrl = this.apiUrl + url; - const response = await fetch(fullUrl, { - ...init, - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async getSettings(): Promise<Settings> { - const result = await this.get<Settings>("/user/settings"); - - result.subscriptions.forEach((s) => { - s.tags = s.tags === null ? [] : s.tags; - }); - return result; - } - - async getContactList(): Promise<ContactList> { - const url = `${this.apiUrl}/contact`; - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - getSettingsByTeam(teamId: string): Promise<Settings> { - return this.get<Settings>(`/teams/${encodeURIComponent(teamId)}/settings`); - } - - async addContact(contact: ContactCreateInfo): Promise<Contact> { - const url = `${this.apiUrl}/contact`; - const response = await fetch(url, { - method: "PUT", - credentials: "same-origin", - body: JSON.stringify(contact), - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async addTeamContact(contact: ContactCreateInfo, team: Team): Promise<Contact> { - const url = `${this.apiUrl}/teams/${encodeURIComponent(team.id)}/contacts`; - const response = await fetch(url, { - method: "POST", - credentials: "same-origin", - body: JSON.stringify(contact), - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async testContact(contactId: string): Promise<void> { - const url = `${this.apiUrl}/contact/${encodeURIComponent(contactId)}/test`; - const response = await fetch(url, { - method: "POST", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async updateContact(contact: Contact): Promise<Contact> { - const url = `${this.apiUrl}/contact/${encodeURIComponent(contact.id)}`; - const response = await fetch(url, { - method: "PUT", - credentials: "same-origin", - body: JSON.stringify(contact), - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async addSubscription(subscription: SubscriptionCreateInfo): Promise<Subscription> { - const url = `${this.apiUrl}/subscription`; - if (subscription.id != null) { - throw new Error("InvalidProgramState: id of subscription must be null or undefined"); - } - const response = await fetch(url, { - method: "PUT", - credentials: "same-origin", - body: JSON.stringify(subscription), - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async addTeamSubscription( - subscription: SubscriptionCreateInfo, - team: Team - ): Promise<Subscription> { - const url = `${this.apiUrl}/teams/${encodeURIComponent(team.id)}/subscriptions`; - if (subscription.id != null) { - throw new Error("InvalidProgramState: id of subscription must be null or undefined"); - } - const response = await fetch(url, { - method: "POST", - credentials: "same-origin", - body: JSON.stringify(subscription), - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async updateSubscription(subscription: Subscription): Promise<Subscription> { - const url = `${this.apiUrl}/subscription/${encodeURIComponent(subscription.id)}`; - const response = await fetch(url, { - method: "PUT", - credentials: "same-origin", - body: JSON.stringify(subscription), - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async testSubscription(subscriptionId: string): Promise<void> { - const url = `${this.apiUrl}/subscription/${encodeURIComponent(subscriptionId)}/test`; - const response = await fetch(url, { - method: "PUT", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async deleteContact(contactId: string): Promise<void> { - const url = `${this.apiUrl}/contact/${encodeURIComponent(contactId)}`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async getPatternList(): Promise<PatternList> { - const url = `${this.apiUrl}/pattern`; - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async delPattern(pattern: string): Promise<void> { - const url = `${this.apiUrl}/pattern/${encodeURIComponent(pattern)}`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async getTagList(): Promise<TagList> { - const url = `${this.apiUrl}/tag`; - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async getTagStats(): Promise<TagStatList> { - const url = `${this.apiUrl}/tag/stats`; - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async delTag(tag: string): Promise<void> { - const url = `${this.apiUrl}/tag/${encodeURIComponent(tag)}`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async getTriggerList( - page: number, - onlyProblems: boolean, - tags: Array<string>, - searchText: string - ): Promise<TriggerList> { - const url = `${this.apiUrl}/trigger/search?${queryString.stringify( - { - p: page, - size: this.triggerListPageSize, - tags, - onlyProblems, - text: searchText, - }, - { arrayFormat: "index", encode: true } - )}`; - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async getTrigger(id: string, params?: { populated: boolean }): Promise<Trigger> { - const url = `${this.apiUrl}/trigger/${encodeURIComponent(id)}${ - params ? `?${queryString.stringify(params)}` : "" - }`; - - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async addTrigger( - data: Partial<Trigger> - ): Promise<{ - [key: string]: string; - }> { - const url = `${this.apiUrl}/trigger`; - const response = await fetch(url, { - method: "PUT", - body: JSON.stringify(data), - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async setTrigger( - id: string, - data: Partial<Trigger> - ): Promise<{ - [key: string]: string; - }> { - const url = `${this.apiUrl}/trigger/${encodeURIComponent(id)}`; - const response = await fetch(url, { - method: "PUT", - body: JSON.stringify(data), - credentials: "same-origin", - }); - - await MoiraApi.checkStatus(response); - return response.json(); - } - - async delTrigger(id: string): Promise<void> { - const url = `${this.apiUrl}/trigger/${encodeURIComponent(id)}`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - validateTarget = async (trigger: Partial<Trigger>): Promise<ValidateTargetsResult> => { - const url = `${this.apiUrl}/trigger/check`; - const response = await fetch(url, { - method: "PUT", - body: JSON.stringify(trigger), - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - }; - - async setMaintenance( - triggerId: string, - data: { - trigger?: number; - metrics?: { - [metric: string]: number; - }; - } - ): Promise<void> { - const url = `${this.apiUrl}/trigger/${encodeURIComponent(triggerId)}/setMaintenance`; - const response = await fetch(url, { - method: "PUT", - body: JSON.stringify(data), - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async getTriggerState(id: string): Promise<TriggerState> { - const url = `${this.apiUrl}/trigger/${encodeURIComponent(id)}/state`; - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async getTriggerEvents(id: string, page: number): Promise<EventList> { - const url = `${this.apiUrl}/event/${encodeURIComponent(id)}?p=${page}&size=${ - this.eventHistoryPageSize - }`; - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async delThrottling(triggerId: string): Promise<void> { - const url = `${this.apiUrl}/trigger/${encodeURIComponent(triggerId)}/throttling`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async delMetric(triggerId: string, metric: string): Promise<void> { - const url = `${this.apiUrl}/trigger/${encodeURIComponent( - triggerId - )}/metrics?name=${encodeURIComponent(metric)}`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async delNoDataMetric(triggerId: string): Promise<void> { - const url = `${this.apiUrl}/trigger/${encodeURIComponent(triggerId)}/metrics/nodata`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async getNotificationList(): Promise<NotificationList> { - const url = `${this.apiUrl}/notification?start=0&end=-1`; - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async delNotification(id: string): Promise<void> { - const url = `${this.apiUrl}/notification?id=${encodeURIComponent(id)}`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async delAllNotifications(): Promise<void> { - const url = `${this.apiUrl}/notification/all`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async delAllNotificationEvents(): Promise<void> { - const url = `${this.apiUrl}/event/all`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async delSubscription(subscriptionId: string): Promise<void> { - const url = `${this.apiUrl}/subscription/${encodeURIComponent(subscriptionId)}`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async getNotifierState(): Promise<NotifierState> { - const url = `${this.apiUrl}/health/notifier`; - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async setNotifierState(status: NotifierState): Promise<NotifierState> { - const url = `${this.apiUrl}/health/notifier`; - const response = await fetch(url, { - method: "PUT", - credentials: "same-origin", - body: JSON.stringify(status), - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async getTeams(): Promise<{ teams: Team[] }> { - const url = `${this.apiUrl}/teams`; - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async addTeam(team: Partial<Team>): Promise<{ id: string }> { - const url = `${this.apiUrl}/teams`; - const response = await fetch(url, { - method: "POST", - body: JSON.stringify(team), - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async updateTeam(team: Team): Promise<{ id: string }> { - const url = `${this.apiUrl}/teams/${encodeURIComponent(team.id)}`; - const response = await fetch(url, { - method: "PATCH", - body: JSON.stringify(team), - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async delTeam(teamId: string): Promise<void> { - const url = `${this.apiUrl}/teams/${encodeURIComponent(teamId)}`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } - - async getUsers(teamId: string): Promise<{ usernames: string[] }> { - const url = `${this.apiUrl}/teams/${encodeURIComponent(teamId)}/users`; - const response = await fetch(url, { - method: "GET", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async addUser(teamId: string, userName: string): Promise<void> { - const url = `${this.apiUrl}/teams/${encodeURIComponent(teamId)}/users`; - const response = await fetch(url, { - method: "POST", - body: JSON.stringify({ usernames: [userName] }), - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - return response.json(); - } - - async delUser(teamId: string, userName: string): Promise<void> { - const url = `${this.apiUrl}/teams/${encodeURIComponent(teamId)}/users/${encodeURIComponent( - userName - )}`; - const response = await fetch(url, { - method: "DELETE", - credentials: "same-origin", - }); - await MoiraApi.checkStatus(response); - } -} diff --git a/src/Api/MoiraApiInjection.tsx b/src/Api/MoiraApiInjection.tsx deleted file mode 100644 index a13e41d94..000000000 --- a/src/Api/MoiraApiInjection.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from "react"; -import MoiraApi from "./MoiraApi"; - -const ApiContext = React.createContext<MoiraApi>(new MoiraApi("/api")); - -export const ApiProvider = ApiContext.Provider; - -export function withMoiraApi<ComponentProps extends { moiraApi: MoiraApi }>( - Component: React.ComponentType<ComponentProps> -): React.ComponentType<Omit<ComponentProps, "moiraApi">> { - function ComponentWithApi(props: Omit<ComponentProps, "moiraApi">) { - const moiraApi = React.useContext(ApiContext); - return <Component {...(props as ComponentProps)} moiraApi={moiraApi} />; - } - - ComponentWithApi.displayName = `withApi(${ - Component.displayName || Component.name || "Component" - })`; - return ComponentWithApi; -} diff --git a/src/Components/CreateSubscriptionModal/CreateSubscriptionModal.tsx b/src/Components/CreateSubscriptionModal/CreateSubscriptionModal.tsx index 7ce9b50f4..2cd81411e 100644 --- a/src/Components/CreateSubscriptionModal/CreateSubscriptionModal.tsx +++ b/src/Components/CreateSubscriptionModal/CreateSubscriptionModal.tsx @@ -6,7 +6,7 @@ import { Fill, RowStack } from "@skbkontur/react-stack-layout"; import { Contact } from "../../Domain/Contact"; import { omitSubscription } from "../../helpers/omitTypes"; import SubscriptionEditor from "../SubscriptionEditor/SubscriptionEditor"; -import { SubscriptionCreateInfo } from "../../Api/MoiraApi"; +import { SubscriptionCreateInfo } from "../../Domain/Subscription"; import FileLoader from "../FileLoader/FileLoader"; import ModalError from "../ModalError/ModalError"; import { useParams } from "react-router"; diff --git a/src/Components/MaintenanceSelect/MaintenanceSelect.tsx b/src/Components/MaintenanceSelect/MaintenanceSelect.tsx index d4985042b..a4b227bda 100644 --- a/src/Components/MaintenanceSelect/MaintenanceSelect.tsx +++ b/src/Components/MaintenanceSelect/MaintenanceSelect.tsx @@ -41,7 +41,11 @@ export default function MaintenanceSelect(props: MaintenanceSelectProps): React. return ( <RenderLayer onClickOutside={handleClose} onFocusOutside={handleClose} active={opened}> <span ref={containerEl} className={cn("container")}> - <Button onClick={() => setOpened(true)} use="link"> + <Button + data-tid="TriggerMaintenanceButton" + onClick={() => setOpened(true)} + use="link" + > {caption} <ArrowTriangleDownIcon color="#6b99d3" /> </Button> {opened ? ( diff --git a/src/Components/MetricListItem/MetricListItem.tsx b/src/Components/MetricListItem/MetricListItem.tsx index f53a083c4..7a4bc8963 100644 --- a/src/Components/MetricListItem/MetricListItem.tsx +++ b/src/Components/MetricListItem/MetricListItem.tsx @@ -1,14 +1,15 @@ import StatusIndicator from "../StatusIndicator/StatusIndicator"; -import { format, fromUnixTime, getUnixTime } from "date-fns"; +import { format, fromUnixTime } from "date-fns"; import MetricValues from "../MetricValues/MetricValues"; import MaintenanceSelect from "../MaintenanceSelect/MaintenanceSelect"; import { Tooltip } from "@skbkontur/react-ui/components/Tooltip"; import UserIcon from "@skbkontur/react-icons/User"; import * as React from "react"; -import { getUTCDate, humanizeDuration } from "../../helpers/DateUtil"; +import { humanizeDuration } from "../../helpers/DateUtil"; import { Metric } from "../../Domain/Metric"; import { useHistory } from "react-router"; import { ConfirmMetricDeletionWithTransformNull } from "../ConfirmMetricDeletionWithTransformNull/ConfirmMetricDeletionWithTransformNull"; +import { maintenanceDelta } from "../../Domain/Trigger"; import classNames from "classnames/bind"; import styles from "../MetricList/MetricList.less"; @@ -19,10 +20,6 @@ function maintenanceCaption(delta: number): React.ReactNode { return <span>{delta <= 0 ? "Maintenance" : humanizeDuration(delta)}</span>; } -function maintenanceDelta(maintenance?: number | null): number { - return (maintenance || 0) - getUnixTime(getUTCDate()); -} - const hideTargetsNames = (values: { [metric: string]: number } | undefined) => { return !values || Object.keys(values).length === 1; }; diff --git a/src/Components/SubscriptionEditor/SubscriptionEditor.tsx b/src/Components/SubscriptionEditor/SubscriptionEditor.tsx index 7ceaedb5a..93ff060ac 100644 --- a/src/Components/SubscriptionEditor/SubscriptionEditor.tsx +++ b/src/Components/SubscriptionEditor/SubscriptionEditor.tsx @@ -2,7 +2,7 @@ import React, { FC } from "react"; import { Toggle } from "@skbkontur/react-ui/components/Toggle"; import { Checkbox } from "@skbkontur/react-ui/components/Checkbox"; import { ValidationWrapperV1, tooltip, ValidationInfo } from "@skbkontur/react-ui-validations"; -import { SubscriptionCreateInfo } from "../../Api/MoiraApi"; +import { SubscriptionCreateInfo } from "../../Domain/Subscription"; import { Contact } from "../../Domain/Contact"; import { Schedule } from "../../Domain/Schedule"; import { Subscription } from "../../Domain/Subscription"; diff --git a/src/Components/TriggerList/TriggerList.tsx b/src/Components/TriggerList/TriggerList.tsx index 1958aaec2..90a9f4b7e 100644 --- a/src/Components/TriggerList/TriggerList.tsx +++ b/src/Components/TriggerList/TriggerList.tsx @@ -18,6 +18,7 @@ type Props = { export default function TriggerList(props: Props): React.ReactElement { const { items, searchMode, onChange, onRemove, history } = props; + return ( <div> {items.length === 0 ? ( diff --git a/src/Components/TriggerListItem/TriggerListItem.tsx b/src/Components/TriggerListItem/TriggerListItem.tsx index d03e81fca..40ff7d1ea 100644 --- a/src/Components/TriggerListItem/TriggerListItem.tsx +++ b/src/Components/TriggerListItem/TriggerListItem.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { useState, useMemo } from "react"; import { History } from "history"; import { format, fromUnixTime } from "date-fns"; import { Link as ReactRouterLink } from "react-router-dom"; @@ -21,7 +22,6 @@ import classNames from "classnames/bind"; import styles from "./TriggerListItem.less"; const cn = classNames.bind(styles); - import _ from "lodash"; type Props = { @@ -32,142 +32,34 @@ type Props = { history: History; }; -type State = { - showMetrics: boolean; - metrics: MetricItemList; - sortingColumn: SortingColumn; - sortingDown: boolean; -}; - -export default class TriggerListItem extends React.Component<Props, State> { - public state: State; - - constructor(props: Props) { - super(props); - this.state = { - showMetrics: false, - metrics: props.data.last_check?.metrics || {}, - sortingColumn: "event", - sortingDown: false, - }; - } - - render(): React.ReactNode { - const { searchMode, data } = this.props; - const { id, name, targets, tags, throttling, highlights } = data; - const { showMetrics } = this.state; - const metrics = this.renderMetrics(); - const searchModeName = highlights && highlights.name; - - return ( - <div className={cn("row", { active: showMetrics })}> - <div - className={cn("state", { active: metrics })} - onClick={() => { - if (metrics) { - this.toggleMetrics(); - } - }} - data-tid="TriggerListItem_status" - > - {this.renderStatus()} - {this.renderCounters()} - </div> - <div className={cn("data")}> - <ReactRouterLink - className={cn("header")} - to={getPageLink("trigger", id)} - data-tid="TriggerListItem_header" - > - <div className={cn("link")}> - <div className={cn("title")}> - {searchMode ? ( - <div - className={cn("name")} - dangerouslySetInnerHTML={{ - __html: sanitize(searchModeName || name), - }} - /> - ) : ( - <div className={cn("name")}>{name}</div> - )} - {throttling !== 0 && ( - <div - className={cn("flag")} - title={`Throttling until - ${format( - fromUnixTime(throttling), - "MMMM d, HH:mm:ss" - )}`} - > - <FlagSolidIcon /> - </div> - )} - </div> - <div className={cn({ targets: true })}> - {targets.map((target) => ( - <div key={target} className={cn("target")}> - {target} - </div> - ))} - </div> - </div> - </ReactRouterLink> - <div className={cn("tags")}> - <TagGroup - onClick={(tag) => { - this.props.history?.push( - `/?${queryString.stringify( - { tags: [tag] }, - { - arrayFormat: "index", - encode: true, - } - )}` - ); - }} - tags={tags} - /> - </div> - {showMetrics && <div className={cn("metrics")}>{metrics}</div>} - </div> - </div> - ); - } +const TriggerListItem: React.FC<Props> = ({ data, searchMode, onChange, onRemove, history }) => { + const [showMetrics, setShowMetrics] = useState(false); + const [sortingColumn, setSortingColumn] = useState<SortingColumn>("event"); + const [sortingDown, setSortingDown] = useState(false); - handleSort(column: SortingColumn) { - const { sortingColumn, sortingDown } = this.state; + const metrics = data.last_check?.metrics; + const handleSort = (column: SortingColumn) => { if (column === sortingColumn) { - this.setState({ sortingDown: !sortingDown }); + setSortingDown((prev) => !prev); } else { - this.setState({ - sortingColumn: column, - sortingDown: true, - }); + setSortingColumn(column); + setSortingDown(true); } - } + }; - getHasExceptionState(): boolean { - const { data } = this.props; - const { state: triggerStatus } = data.last_check || {}; - return triggerStatus === Status.EXCEPTION; - } + const hasExceptionState = data.last_check?.state === Status.EXCEPTION; - toggleMetrics(): void { - const { showMetrics } = this.state; - this.setState({ showMetrics: !showMetrics }); - } + const filterMetricsByStatus = useMemo( + () => (status: Status): MetricItemList => + _.pickBy(metrics, (metric) => metric.state === status), + [metrics] + ); - filterMetricsByStatus(status: Status): MetricItemList { - const { metrics } = this.state; - return _.pickBy(metrics, (metric) => metric.state === status); - } - - renderCounters(): React.ReactElement { + const renderCounters = (): React.ReactElement => { const counters = StatusesInOrder.map((status) => ({ status, - count: Object.keys(this.filterMetricsByStatus(status)).length, + count: Object.keys(filterMetricsByStatus(status)).length, })) .filter(({ count }) => count !== 0) .map(({ status, count }) => ( @@ -180,33 +72,29 @@ export default class TriggerListItem extends React.Component<Props, State> { {counters.length !== 0 ? counters : <span className={cn("NA")}>N/A</span>} </div> ); - } + }; - renderStatus(): React.ReactElement { - const { data } = this.props; - const { state: triggerStatus } = data.last_check || {}; + const renderStatus = (): React.ReactElement => { + const triggerStatus = data.last_check?.state; const metricStatuses = StatusesInOrder.filter( - (x) => Object.keys(this.filterMetricsByStatus(x)).length !== 0 + (x) => Object.keys(filterMetricsByStatus(x)).length !== 0 ); const notOkStatuses = metricStatuses.filter((x) => x !== Status.OK); - let statuses: Status[]; - if (triggerStatus && (triggerStatus !== Status.OK || metricStatuses.length === 0)) { - statuses = [triggerStatus]; - } else if (notOkStatuses.length !== 0) { - statuses = notOkStatuses; - } else { - statuses = [Status.OK]; - } + const statuses = + triggerStatus && (triggerStatus !== Status.OK || metricStatuses.length === 0) + ? [triggerStatus] + : notOkStatuses.length !== 0 + ? notOkStatuses + : [Status.OK]; return ( <div className={cn("indicator")}> <StatusIndicator statuses={statuses} /> </div> ); - } + }; - renderExceptionHelpMessage(): React.ReactElement { - const { data } = this.props; - const hasExpression = data.expression != null && data.expression !== ""; + const renderExceptionHelpMessage = (): React.ReactElement => { + const hasExpression = !!data.expression; const hasMultipleTargets = data.targets.length > 1; return ( <div className={cn("exception-message")}> @@ -219,43 +107,110 @@ export default class TriggerListItem extends React.Component<Props, State> { page. </div> ); - } + }; - renderMetrics(): React.ReactNode { - const { onChange, onRemove, data } = this.props; - const { sortingColumn, sortingDown } = this.state; - if (!onChange || !onRemove) { - return null; - } + const renderMetrics = (): React.ReactNode => { + if (!onChange || !onRemove) return null; const statuses = StatusesInOrder.filter( - (x) => Object.keys(this.filterMetricsByStatus(x)).length !== 0 + (x) => Object.keys(filterMetricsByStatus(x)).length !== 0 ); - if (statuses.length === 0) { - return null; - } - const metrics: Array<React.ReactElement> = statuses.map((status: Status) => ( - <Tab key={status} id={status} label={getStatusCaption(status)}> - <MetricListView - items={sortMetrics( - this.filterMetricsByStatus(status), - sortingColumn, - sortingDown - )} - sortingColumn={sortingColumn} - onSort={(column) => this.handleSort(column)} - sortingDown={sortingDown} - onChange={(metric: string, maintenance: number) => - onChange?.(data.id, metric, maintenance) - } - onRemove={(metric: string) => onRemove(metric)} - /> - </Tab> - )); + if (statuses.length === 0) return null; + return ( <div className={cn("metrics")}> - {this.getHasExceptionState() && this.renderExceptionHelpMessage()} - <Tabs value={statuses[0]}>{metrics}</Tabs> + {hasExceptionState && renderExceptionHelpMessage()} + <Tabs value={statuses[0]}> + {statuses.map((status) => ( + <Tab key={status} id={status} label={getStatusCaption(status)}> + <MetricListView + items={sortMetrics( + filterMetricsByStatus(status), + sortingColumn, + sortingDown + )} + sortingColumn={sortingColumn} + onSort={handleSort} + sortingDown={sortingDown} + onChange={(metric, maintenance) => + onChange?.(data.id, metric, maintenance) + } + onRemove={onRemove} + /> + </Tab> + ))} + </Tabs> </div> ); - } -} + }; + + const searchModeName = data.highlights?.name; + + return ( + <div className={cn("row", { active: showMetrics })}> + <div + className={cn("state", { active: metrics })} + onClick={() => metrics && setShowMetrics((prev) => !prev)} + data-tid="TriggerListItem_status" + > + {renderStatus()} + {renderCounters()} + </div> + <div className={cn("data")}> + <ReactRouterLink + className={cn("header")} + to={getPageLink("trigger", data.id)} + data-tid="TriggerListItem_header" + > + <div className={cn("link")}> + <div className={cn("title")}> + {searchMode ? ( + <div + className={cn("name")} + dangerouslySetInnerHTML={{ + __html: sanitize(searchModeName || data.name), + }} + /> + ) : ( + <div className={cn("name")}>{data.name}</div> + )} + {data.throttling !== 0 && ( + <div + className={cn("flag")} + title={`Throttling until ${format( + fromUnixTime(data.throttling), + "MMMM d, HH:mm:ss" + )}`} + > + <FlagSolidIcon /> + </div> + )} + </div> + <div className={cn("targets")}> + {data.targets.map((target) => ( + <div key={target} className={cn("target")}> + {target} + </div> + ))} + </div> + </div> + </ReactRouterLink> + <div className={cn("tags")}> + <TagGroup + onClick={(tag) => + history.push( + `/?${queryString.stringify( + { tags: [tag] }, + { arrayFormat: "index", encode: true } + )}` + ) + } + tags={data.tags} + /> + </div> + {showMetrics && <div className={cn("metrics")}>{renderMetrics()}</div>} + </div> + </div> + ); +}; + +export default TriggerListItem; diff --git a/src/Components/TriggerSaveWarningModal/TriggerSaveWarningModal.tsx b/src/Components/TriggerSaveWarningModal/TriggerSaveWarningModal.tsx index 7dd02145d..c5039ebcd 100644 --- a/src/Components/TriggerSaveWarningModal/TriggerSaveWarningModal.tsx +++ b/src/Components/TriggerSaveWarningModal/TriggerSaveWarningModal.tsx @@ -9,7 +9,7 @@ export function TriggerSaveWarningModal({ }: { isOpen: boolean; onClose: () => void; - onSave: () => Promise<void>; + onSave: () => void; }) { return ( (isOpen && ( diff --git a/src/Containers/HeaderContainer.tsx b/src/Containers/HeaderContainer.tsx index 63ea64d16..3031a7f59 100644 --- a/src/Containers/HeaderContainer.tsx +++ b/src/Containers/HeaderContainer.tsx @@ -1,56 +1,25 @@ -import * as React from "react"; -import MoiraApi from "../Api/MoiraApi"; -import { withMoiraApi } from "../Api/MoiraApiInjection"; +import React from "react"; import Bar from "../Components/Bar/Bar"; import Header from "../Components/Header/Header"; import MoiraServiceStates from "../Domain/MoiraServiceStates"; +import { useGetNotifierStateQuery } from "../services/NotifierApi"; -type Props = { - moiraApi: MoiraApi; +interface IHeaderContainerProps { className: string; -}; - -type State = { - notifierStateMessage?: string; -}; - -class HeaderContainer extends React.Component<Props, State> { - state: State = {}; - - componentDidMount() { - this.getData(); - } +} - render(): React.ReactElement { - const { notifierStateMessage } = this.state; - const { className } = this.props; - return ( - <div className={className}> - {notifierStateMessage && <Bar message={notifierStateMessage} />} - <Header /> - </div> - ); - } +export const HeaderContainer: React.FC<IHeaderContainerProps> = ({ className }) => { + const { data: notifierState } = useGetNotifierStateQuery(undefined, { + refetchOnMountOrArgChange: true, + }); - async getData() { - const { moiraApi } = this.props; - try { - const { state, message } = await moiraApi.getNotifierState(); - switch (state) { - case MoiraServiceStates.OK: - this.setState({ notifierStateMessage: undefined }); - break; - case MoiraServiceStates.ERROR: - this.setState({ notifierStateMessage: message }); - break; - default: - break; - } - } catch (error) { - console.error(error); - // ToDo: do something with this error - } - } -} + const notifierStateMessage = + notifierState?.state === MoiraServiceStates.ERROR ? notifierState?.message : undefined; -export default withMoiraApi(HeaderContainer); + return ( + <div className={className}> + {notifierStateMessage && <Bar message={notifierStateMessage} />} + <Header /> + </div> + ); +}; diff --git a/src/Containers/NotificationListContainer.tsx b/src/Containers/NotificationListContainer.tsx index 2c19deeb2..da9cb1fc0 100644 --- a/src/Containers/NotificationListContainer.tsx +++ b/src/Containers/NotificationListContainer.tsx @@ -3,75 +3,44 @@ import { Button } from "@skbkontur/react-ui/components/Button"; import { Flexbox } from "../Components/Flexbox/FlexBox"; import { Toggle } from "@skbkontur/react-ui/components/Toggle"; import TrashIcon from "@skbkontur/react-icons/Trash"; -import MoiraApi from "../Api/MoiraApi"; -import { withMoiraApi } from "../Api/MoiraApiInjection"; import MoiraServiceStates from "../Domain/MoiraServiceStates"; import { Layout, LayoutContent, LayoutTitle } from "../Components/Layout/Layout"; import NotificationList from "../Components/NotificationList/NotificationList"; import { setDocumentTitle } from "../helpers/setDocumentTitle"; -import { NotificationsState, UIState } from "../store/selectors"; -import { - deleteNotification, - setNotifierEnabled, - deleteAllNotifications, -} from "../store/Reducers/NotificationListContainerReducer.slice"; -import { toggleLoading, setError } from "../store/Reducers/UIReducer.slice"; -import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { UIState } from "../store/selectors"; +import { useAppSelector } from "../store/hooks"; import { useLoadNotificationsData } from "../hooks/useLoadNotificationsData"; import { composeNotifications } from "../helpers/composeNotifications"; import { ConfirmModalHeaderData } from "../Domain/Global"; import useConfirmModal from "../hooks/useConfirmModal"; +import { + useDeleteAllNotificationEventsMutation, + useDeleteAllNotificationsMutation, + useDeleteNotificationMutation, +} from "../services/NotificationsApi"; +import { useSetNotifierStateMutation } from "../services/NotifierApi"; -type TProps = { moiraApi: MoiraApi }; - -const NotificationListContainer: FC<TProps> = ({ moiraApi }) => { - const dispatch = useAppDispatch(); - const { notificationList, notifierEnabled } = useAppSelector(NotificationsState); +const NotificationListContainer: FC = () => { const { isLoading, error } = useAppSelector(UIState); - const { loadNotificationsData } = useLoadNotificationsData(moiraApi); + const { notifierEnabled, notificationList, notificationAmount } = useLoadNotificationsData(); + const [deleteNotification] = useDeleteNotificationMutation(); + const [deleteAllNotifications] = useDeleteAllNotificationsMutation(); + const [deleleteAllNotificationEvents] = useDeleteAllNotificationEventsMutation(); + const [setNotifierState] = useSetNotifierStateMutation(); const [ConfirmModal, setModalData] = useConfirmModal(); - const notificationAmount = notificationList.length; const layoutTitle = `Notifications ${notificationAmount}`; - const removeNotification = async (id: string) => { - dispatch(toggleLoading(true)); - try { - await moiraApi.delNotification(id); - dispatch(deleteNotification(id)); - } catch (error) { - dispatch(setError(error.message)); - } finally { - dispatch(toggleLoading(false)); - } - }; - const removeAllNotifications = async () => { setModalData({ isOpen: false }); - dispatch(toggleLoading(true)); - try { - await moiraApi.delAllNotificationEvents(); - await moiraApi.delAllNotifications(); - dispatch(deleteAllNotifications()); - } catch (error) { - dispatch(setError(error.message)); - } finally { - dispatch(toggleLoading(false)); - } + deleteAllNotifications(); + deleleteAllNotificationEvents(); }; const toggleNotifier = async (enable: boolean) => { setModalData({ isOpen: false }); - dispatch(toggleLoading(true)); - try { - const state = enable ? MoiraServiceStates.OK : MoiraServiceStates.ERROR; - await moiraApi.setNotifierState({ state }); - dispatch(setNotifierEnabled(enable)); - } catch (error) { - dispatch(setError(error.message)); - } finally { - dispatch(toggleLoading(false)); - } + const state = enable ? MoiraServiceStates.OK : MoiraServiceStates.ERROR; + setNotifierState({ state }); }; const handleDisableNotifier = () => { @@ -87,20 +56,20 @@ const NotificationListContainer: FC<TProps> = ({ moiraApi }) => { }; const onRemoveAllNotificationsBtnClick = () => { - setModalData({ - isOpen: true, - header: ConfirmModalHeaderData.deleteAllNotifications(notificationAmount), - button: { - text: "Delete", - use: "danger", - onConfirm: removeAllNotifications, - }, - }); + notificationAmount && + setModalData({ + isOpen: true, + header: ConfirmModalHeaderData.deleteAllNotifications(notificationAmount), + button: { + text: "Delete", + use: "danger", + onConfirm: removeAllNotifications, + }, + }); }; useEffect(() => { setDocumentTitle("Notifications"); - loadNotificationsData(); }, []); return ( @@ -129,7 +98,7 @@ const NotificationListContainer: FC<TProps> = ({ moiraApi }) => { {notificationList && ( <NotificationList items={composeNotifications(notificationList)} - onRemove={removeNotification} + onRemove={(id) => deleteNotification({ id })} /> )} </LayoutContent> @@ -137,4 +106,4 @@ const NotificationListContainer: FC<TProps> = ({ moiraApi }) => { ); }; -export default withMoiraApi(NotificationListContainer); +export default NotificationListContainer; diff --git a/src/Containers/PatternListContainer.tsx b/src/Containers/PatternListContainer.tsx index 9ab552e0c..168691e29 100644 --- a/src/Containers/PatternListContainer.tsx +++ b/src/Containers/PatternListContainer.tsx @@ -1,61 +1,38 @@ -import React, { useState, useEffect } from "react"; -import MoiraApi from "../Api/MoiraApi"; -import { Pattern } from "../Domain/Pattern"; -import { withMoiraApi } from "../Api/MoiraApiInjection"; +import React, { useEffect } from "react"; import PatternList from "../Components/PatternList/PatternList"; import { Layout, LayoutContent, LayoutTitle } from "../Components/Layout/Layout"; import { setDocumentTitle } from "../helpers/setDocumentTitle"; -import { toggleLoading, setError } from "../store/Reducers/UIReducer.slice"; -import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { useAppSelector } from "../store/hooks"; import { UIState } from "../store/selectors"; import { useSortData } from "../hooks/useSortData"; +import { useDeletePatternMutation, useGetPatternsQuery } from "../services/PatternsApi"; -export type TPatternListContainerProps = { moiraApi: MoiraApi }; - -const PatternListContainer: React.FC<TPatternListContainerProps> = ({ moiraApi }) => { - const dispatch = useAppDispatch(); +const PatternListContainer: React.FC = () => { const { isLoading, error } = useAppSelector(UIState); - const [list, setList] = useState<Pattern[] | undefined>(); - const { sortedData, sortConfig, handleSort } = useSortData(list ?? [], "metrics"); + const { data: patterns } = useGetPatternsQuery(); + const [deletePattern] = useDeletePatternMutation(); + + const { sortedData, sortConfig, handleSort } = useSortData(patterns ?? [], "metrics"); useEffect(() => { setDocumentTitle("Patterns"); - getData(); }, []); - const getData = async () => { - dispatch(toggleLoading(true)); - try { - const { list } = await moiraApi.getPatternList(); - setList(list); - } catch (error) { - dispatch(setError(error.message)); - } finally { - dispatch(toggleLoading(false)); - } - }; - - const removePattern = async (pattern: string) => { - dispatch(toggleLoading(true)); - await moiraApi.delPattern(pattern); - getData(); - }; - return ( <Layout loading={isLoading} error={error}> <LayoutContent> <LayoutTitle>Patterns</LayoutTitle> - {list && ( + { <PatternList items={sortedData} onSort={handleSort} sortConfig={sortConfig} - onRemove={removePattern} + onRemove={deletePattern} /> - )} + } </LayoutContent> </Layout> ); }; -export default withMoiraApi(PatternListContainer); +export default PatternListContainer; diff --git a/src/Containers/SettingsContainer.tsx b/src/Containers/SettingsContainer.tsx index 7bc6fa9b8..db800264f 100644 --- a/src/Containers/SettingsContainer.tsx +++ b/src/Containers/SettingsContainer.tsx @@ -84,7 +84,7 @@ const SettingsContainer: FC<ISettingsContainerProps> = ({ isTeamMember, history {settings && tags && ( <SubscriptionListContainer teams={teams} - tags={tags.list} + tags={tags} contacts={settings.contacts} subscriptions={settings.subscriptions} /> diff --git a/src/Containers/TriggerAddContainer.tsx b/src/Containers/TriggerAddContainer.tsx index ead8633e0..2a08616de 100644 --- a/src/Containers/TriggerAddContainer.tsx +++ b/src/Containers/TriggerAddContainer.tsx @@ -1,11 +1,8 @@ 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/components/Button"; import { Fill, RowStack as LayoutRowStack } from "@skbkontur/react-stack-layout"; import { useSaveTrigger } from "../hooks/useSaveTrigger"; -import MoiraApi from "../Api/MoiraApi"; -import { withMoiraApi } from "../Api/MoiraApiInjection"; import { DEFAULT_TRIGGER_TTL, Trigger, TriggerSource } from "../Domain/Trigger"; import { getPageLink } from "../Domain/Global"; import { Status } from "../Domain/Status"; @@ -17,17 +14,17 @@ import TriggerEditForm from "../Components/TriggerEditForm/TriggerEditForm"; import { RowStack, ColumnStack, Fit } from "../Components/ItemsStack/ItemsStack"; import FileLoader from "../Components/FileLoader/FileLoader"; import { useValidateTarget } from "../hooks/useValidateTarget"; -import { - setError, - setIsLoading, - setIsSaveButtonDisabled, - setIsSaveModalVisible, - useTriggerFormContainerReducer, -} from "../hooks/useTriggerFormContainerReducer"; import { TriggerSaveWarningModal } from "../Components/TriggerSaveWarningModal/TriggerSaveWarningModal"; import { setDocumentTitle } from "../helpers/setDocumentTitle"; -import { useAppSelector } from "../store/hooks"; -import { ConfigState } from "../store/selectors"; +import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { ConfigState, TriggerFormState, UIState } from "../store/selectors"; +import { useGetTagsQuery } from "../services/TagsApi"; +import { setError } from "../store/Reducers/UIReducer.slice"; +import { + setIsSaveModalVisible, + setIsSaveButtonDisabled, +} from "../store/Reducers/TriggerFormReducer.slice"; +import { useHistory } from "react-router"; const defaultTrigger: Partial<Trigger> = { name: "", @@ -61,16 +58,17 @@ const defaultTrigger: Partial<Trigger> = { alone_metrics: {}, }; -type Props = RouteComponentProps & { moiraApi: MoiraApi }; - -const TriggerAddContainer = (props: Props) => { +const TriggerAddContainer = () => { + const history = useHistory(); const { config } = useAppSelector(ConfigState); - const [state, dispatch] = useTriggerFormContainerReducer(); + const { validationResult, isSaveModalVisible } = useAppSelector(TriggerFormState); + const { isLoading, error } = useAppSelector(UIState); + const dispatch = useAppDispatch(); const [trigger, setTrigger] = useState<Partial<Trigger>>(defaultTrigger); - const [tags, setTags] = useState<string[] | undefined>(undefined); const validationContainer = useRef<ValidationContainer>(null); - const validateTarget = useValidateTarget(props.moiraApi, dispatch, props.history); - const saveTrigger = useSaveTrigger(props.moiraApi, dispatch, props.history); + const { data: tags } = useGetTagsQuery(); + const validateTarget = useValidateTarget(dispatch, history); + const saveTrigger = useSaveTrigger(history); const handleSubmit = async () => { const isFormValid = await validationContainer.current?.validate(); @@ -120,35 +118,30 @@ const TriggerAddContainer = (props: Props) => { } }; - const getData = async () => { + const setTriggerWithSearchTags = () => { const localDataString = localStorage.getItem("moiraSettings"); const { tags: localTags } = localDataString ? JSON.parse(localDataString) : { tags: [] }; - try { - const { list } = await props.moiraApi.getTagList(); - setTrigger((prev) => { - return { ...prev, tags: localTags }; - }); - setTags(list); - } catch (error) { - dispatch(setError(error.message)); - } finally { - dispatch(setIsLoading(false)); - } + setTrigger((prev) => { + return { ...prev, tags: localTags }; + }); }; useEffect(() => { setDocumentTitle("Add trigger"); - getData(); + setTriggerWithSearchTags(); }, []); return ( - <Layout loading={state.isLoading} error={state.error}> + <Layout loading={isLoading} error={error}> <LayoutContent> <TriggerSaveWarningModal - isOpen={state.isSaveModalVisible} + isOpen={isSaveModalVisible} onClose={() => dispatch(setIsSaveModalVisible(false))} - onSave={() => saveTrigger(trigger)} + onSave={() => { + saveTrigger(trigger); + dispatch(setIsSaveModalVisible(false)); + }} /> <LayoutRowStack baseline block gap={6} style={{ maxWidth: "800px" }}> <Fill> @@ -168,7 +161,7 @@ const TriggerAddContainer = (props: Props) => { metricSourceClusters={config.metric_source_clusters} tags={tags || []} onChange={handleChange} - validationResult={state.validationResult} + validationResult={validationResult} /> )} </ValidationContainer> @@ -198,4 +191,4 @@ const TriggerAddContainer = (props: Props) => { ); }; -export default withMoiraApi(TriggerAddContainer); +export default TriggerAddContainer; diff --git a/src/Containers/TriggerDuplicateContainer.tsx b/src/Containers/TriggerDuplicateContainer.tsx index 7201505c6..f98806105 100644 --- a/src/Containers/TriggerDuplicateContainer.tsx +++ b/src/Containers/TriggerDuplicateContainer.tsx @@ -3,55 +3,58 @@ import { RouteComponentProps } from "react-router"; import { ValidationContainer } from "@skbkontur/react-ui-validations"; import { Button } from "@skbkontur/react-ui/components/Button"; import { useSaveTrigger } from "../hooks/useSaveTrigger"; -import MoiraApi from "../Api/MoiraApi"; -import { withMoiraApi } from "../Api/MoiraApiInjection"; import TriggerSource, { Trigger } from "../Domain/Trigger"; import { getPageLink } from "../Domain/Global"; import RouterLink from "../Components/RouterLink/RouterLink"; import { Layout, LayoutContent, LayoutTitle } from "../Components/Layout/Layout"; import TriggerEditForm from "../Components/TriggerEditForm/TriggerEditForm"; import { ColumnStack, RowStack, Fit } from "../Components/ItemsStack/ItemsStack"; -import { - setError, - setIsLoading, - setIsSaveButtonDisabled, - setIsSaveModalVisible, - useTriggerFormContainerReducer, -} from "../hooks/useTriggerFormContainerReducer"; +import { setError } from "../store/Reducers/UIReducer.slice"; import { useValidateTarget } from "../hooks/useValidateTarget"; import { TriggerSaveWarningModal } from "../Components/TriggerSaveWarningModal/TriggerSaveWarningModal"; import { setDocumentTitle } from "../helpers/setDocumentTitle"; -import { useAppSelector } from "../store/hooks"; -import { ConfigState } from "../store/selectors"; +import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { ConfigState, TriggerFormState, UIState } from "../store/selectors"; +import { useGetTriggerQuery } from "../services/TriggerApi"; +import { useGetTagsQuery } from "../services/TagsApi"; +import { + setIsSaveModalVisible, + setIsSaveButtonDisabled, +} from "../store/Reducers/TriggerFormReducer.slice"; + +type Props = RouteComponentProps<{ id: string }>; -// TODO check id wasn't undefined -type Props = RouteComponentProps<{ id?: string }> & { moiraApi: MoiraApi }; +const cleanTrigger = (sourceTrigger: Trigger): Partial<Trigger> => { + const trigger: Partial<Trigger> = { ...sourceTrigger }; + + delete trigger.id; + delete trigger.last_check; + delete trigger.throttling; + + return { + ...trigger, + name: `${trigger.name} (copy)`, + sched: trigger.sched + ? { ...trigger.sched, tzOffset: new Date().getTimezoneOffset() } + : undefined, + }; +}; const TriggerDuplicateContainer = (props: Props) => { const { config } = useAppSelector(ConfigState); - const [state, dispatch] = useTriggerFormContainerReducer(); + const { isSaveModalVisible, validationResult } = useAppSelector(TriggerFormState); + const { isLoading, error } = useAppSelector(UIState); + const dispatch = useAppDispatch(); + const { id } = props.match.params; + const { data: sourceTrigger } = useGetTriggerQuery({ + triggerId: id, + }); + const { data: tags } = useGetTagsQuery(); const [trigger, setTrigger] = useState<Partial<Trigger> | undefined>(undefined); - const [tags, setTags] = useState<string[] | undefined>(undefined); const validationContainer = useRef<ValidationContainer>(null); - const validateTarget = useValidateTarget(props.moiraApi, dispatch, props.history); - const saveTrigger = useSaveTrigger(props.moiraApi, dispatch, props.history); - - const cleanTrigger = (sourceTrigger: Trigger): Partial<Trigger> => { - const trigger: Partial<Trigger> = { ...sourceTrigger }; - - delete trigger.id; - delete trigger.last_check; - delete trigger.throttling; - - return { - ...trigger, - name: `${trigger.name} (copy)`, - sched: trigger.sched - ? { ...trigger.sched, tzOffset: new Date().getTimezoneOffset() } - : undefined, - }; - }; + const validateTarget = useValidateTarget(dispatch, props.history); + const saveTrigger = useSaveTrigger(props.history); const handleSubmit = async () => { const isFormValid = await validationContainer.current?.validate(); @@ -82,49 +85,31 @@ const TriggerDuplicateContainer = (props: Props) => { return { ...prev, ...update }; }); dispatch(setError(null)); - if (update.targets) { dispatch(setIsSaveButtonDisabled(false)); } }; - const getData = async () => { - const { id } = props.match.params; - if (typeof id !== "string") { - dispatch(setError("Wrong trigger id")); - dispatch(setIsLoading(false)); - return; - } - - try { - const [sourceTrigger, { list }] = await Promise.all([ - props.moiraApi.getTrigger(id), - props.moiraApi.getTagList(), - ]); - - const trigger = cleanTrigger(sourceTrigger); - setTrigger(trigger); - setTags(list); - } catch (error) { - dispatch(setError(error.message)); - } finally { - dispatch(setIsLoading(false)); - } - }; - useEffect(() => { setDocumentTitle("Duplicate trigger"); - dispatch(setIsLoading(true)); - getData(); - }, []); + + if (sourceTrigger) { + setTrigger(cleanTrigger(sourceTrigger)); + } else { + setTrigger(undefined); + } + }, [sourceTrigger]); return ( - <Layout loading={state.isLoading} error={state.error}> + <Layout loading={isLoading} error={error}> <LayoutContent> <TriggerSaveWarningModal - isOpen={state.isSaveModalVisible} + isOpen={isSaveModalVisible} onClose={() => dispatch(setIsSaveModalVisible(false))} - onSave={() => saveTrigger(trigger)} + onSave={() => { + saveTrigger(trigger); + dispatch(setIsSaveModalVisible(false)); + }} /> <LayoutTitle>Duplicate trigger</LayoutTitle> {trigger && ( @@ -139,7 +124,7 @@ const TriggerDuplicateContainer = (props: Props) => { remoteAllowed={config.remoteAllowed} metricSourceClusters={config.metric_source_clusters} onChange={handleChange} - validationResult={state.validationResult} + validationResult={validationResult} /> )} </ValidationContainer> @@ -175,4 +160,4 @@ const TriggerDuplicateContainer = (props: Props) => { ); }; -export default withMoiraApi(TriggerDuplicateContainer); +export default TriggerDuplicateContainer; diff --git a/src/Containers/TriggerEditContainer.tsx b/src/Containers/TriggerEditContainer.tsx index b0f2a892a..d324a33fd 100644 --- a/src/Containers/TriggerEditContainer.tsx +++ b/src/Containers/TriggerEditContainer.tsx @@ -3,38 +3,43 @@ import { RouteComponentProps } from "react-router"; import { ValidationContainer } from "@skbkontur/react-ui-validations"; import { Button } from "@skbkontur/react-ui"; import { useSaveTrigger } from "../hooks/useSaveTrigger"; -import MoiraApi from "../Api/MoiraApi"; -import { withMoiraApi } from "../Api/MoiraApiInjection"; import { Trigger, TriggerSource } from "../Domain/Trigger"; import { getPageLink } from "../Domain/Global"; import RouterLink from "../Components/RouterLink/RouterLink"; import { Layout, LayoutContent, LayoutTitle } from "../Components/Layout/Layout"; import TriggerEditForm from "../Components/TriggerEditForm/TriggerEditForm"; import { ColumnStack, RowStack, Fit } from "../Components/ItemsStack/ItemsStack"; -import { - setError, - setIsLoading, - setIsSaveButtonDisabled, - setIsSaveModalVisible, - useTriggerFormContainerReducer, -} from "../hooks/useTriggerFormContainerReducer"; import { useValidateTarget } from "../hooks/useValidateTarget"; import { TriggerSaveWarningModal } from "../Components/TriggerSaveWarningModal/TriggerSaveWarningModal"; import { setDocumentTitle } from "../helpers/setDocumentTitle"; -import { useAppSelector } from "../store/hooks"; -import { ConfigState } from "../store/selectors"; +import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { ConfigState, TriggerFormState, UIState } from "../store/selectors"; +import { setError } from "../store/Reducers/UIReducer.slice"; +import { useGetTagsQuery } from "../services/TagsApi"; +import { useGetTriggerQuery } from "../services/TriggerApi"; +import { + setIsSaveButtonDisabled, + setIsSaveModalVisible, +} from "../store/Reducers/TriggerFormReducer.slice"; -type Props = RouteComponentProps<{ id?: string }> & { moiraApi: MoiraApi }; +type Props = RouteComponentProps<{ id: string }>; const TriggerEditContainer = (props: Props) => { - const [state, dispatch] = useTriggerFormContainerReducer(); const [trigger, setTrigger] = useState<Trigger | undefined>(undefined); - const [tags, setTags] = useState<string[] | undefined>(undefined); const { config } = useAppSelector(ConfigState); + const { validationResult, isSaveModalVisible } = useAppSelector(TriggerFormState); + const { isLoading, error } = useAppSelector(UIState); + const dispatch = useAppDispatch(); + + const { id } = props.match.params; + const { data: sourceTrigger } = useGetTriggerQuery({ + triggerId: id, + }); + const { data: tags } = useGetTagsQuery(); const validationContainer = useRef<ValidationContainer>(null); - const validateTarget = useValidateTarget(props.moiraApi, dispatch, props.history); - const saveTrigger = useSaveTrigger(props.moiraApi, dispatch, props.history); + const validateTarget = useValidateTarget(dispatch, props.history); + const saveTrigger = useSaveTrigger(props.history); const handleSubmit = async () => { const isFormValid = await validationContainer.current?.validate(); @@ -68,41 +73,26 @@ const TriggerEditContainer = (props: Props) => { } }; - const getData = async () => { - if (typeof props.match.params.id !== "string") { - dispatch(setError("Wrong trigger id")); - dispatch(setIsLoading(false)); - return; - } - - try { - const [trigger, { list }] = await Promise.all([ - props.moiraApi.getTrigger(props.match.params.id), - props.moiraApi.getTagList(), - ]); - - setTrigger(trigger); - setTags(list); - } catch (error) { - dispatch(setError(error.message)); - } finally { - dispatch(setIsLoading(false)); - } - }; - useEffect(() => { setDocumentTitle("Edit trigger"); - dispatch(setIsLoading(true)); - getData(); - }, []); + + if (sourceTrigger) { + setTrigger(sourceTrigger); + } else { + setTrigger(undefined); + } + }, [sourceTrigger]); return ( - <Layout loading={state.isLoading} error={state.error}> + <Layout loading={isLoading} error={error}> <LayoutContent> <TriggerSaveWarningModal - isOpen={state.isSaveModalVisible} + isOpen={isSaveModalVisible} onClose={() => dispatch(setIsSaveModalVisible(false))} - onSave={() => saveTrigger(trigger)} + onSave={() => { + saveTrigger(trigger); + dispatch(setIsSaveModalVisible(false)); + }} /> <LayoutTitle>Edit trigger</LayoutTitle> {trigger && ( @@ -117,7 +107,7 @@ const TriggerEditContainer = (props: Props) => { remoteAllowed={config.remoteAllowed} metricSourceClusters={config.metric_source_clusters} onChange={handleChange} - validationResult={state.validationResult} + validationResult={validationResult} /> )} </ValidationContainer> @@ -129,7 +119,6 @@ const TriggerEditContainer = (props: Props) => { use="primary" onClick={handleSubmit} data-tid="Save Trigger" - disabled={state.isSaveButtonDisabled} > Save trigger </Button> @@ -149,4 +138,4 @@ const TriggerEditContainer = (props: Props) => { ); }; -export default withMoiraApi(TriggerEditContainer); +export default TriggerEditContainer; diff --git a/src/Domain/Maintenance.test.ts b/src/Domain/Maintenance.test.ts deleted file mode 100644 index f389a5e26..000000000 --- a/src/Domain/Maintenance.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import MockDate from "mockdate"; -import MoiraApi from "../Api/MoiraApi"; -import { calculateMaintenanceTime, Maintenance, setTriggerMaintenance } from "./Maintenance"; - -const mockFn = jest.fn(); - -describe("Maintenance", () => { - const triggerId = "f96c5608-bd00-4ea2-abd5-be677cdf7e03"; - const api = ({ setMaintenance: mockFn } as unknown) as MoiraApi; - - beforeEach(() => { - MockDate.set("Tue Jun 09 2020 09:00:00 GMT+0300"); - }); - - afterEach(() => { - MockDate.reset(); - mockFn.mockClear(); - }); - - it(`call setTriggerMaintenance with off`, async () => { - await setTriggerMaintenance(api, triggerId, calculateMaintenanceTime(Maintenance.off)); - expect(api.setMaintenance).toBeCalledWith(triggerId, { trigger: 0 }); - }); - it(`call setTriggerMaintenance with quarterHour "15 min"`, async () => { - await setTriggerMaintenance( - api, - triggerId, - calculateMaintenanceTime(Maintenance.quarterHour) - ); - expect(api.setMaintenance).toBeCalledWith(triggerId, { trigger: 1591683300 }); - }); - it(`call setTriggerMaintenance with oneHour "1 hour"`, async () => { - await setTriggerMaintenance(api, triggerId, calculateMaintenanceTime(Maintenance.oneHour)); - expect(api.setMaintenance).toBeCalledWith(triggerId, { trigger: 1591686000 }); - }); - it(`call setTriggerMaintenance with oneDay "1 day"`, async () => { - await setTriggerMaintenance(api, triggerId, calculateMaintenanceTime(Maintenance.oneDay)); - expect(api.setMaintenance).toBeCalledWith(triggerId, { trigger: 1591768800 }); - }); - it(`call setTriggerMaintenance with oneWeek "1 week"`, async () => { - await setTriggerMaintenance(api, triggerId, calculateMaintenanceTime(Maintenance.oneWeek)); - expect(api.setMaintenance).toBeCalledWith(triggerId, { trigger: 1592287200 }); - }); - it(`call setTriggerMaintenance with oneMonth "1 month"`, async () => { - await setTriggerMaintenance(api, triggerId, calculateMaintenanceTime(Maintenance.oneMonth)); - expect(api.setMaintenance).toBeCalledWith(triggerId, { trigger: 1594274400 }); - }); -}); diff --git a/src/Domain/Maintenance.ts b/src/Domain/Maintenance.ts index c97aa4e63..39aa665f8 100644 --- a/src/Domain/Maintenance.ts +++ b/src/Domain/Maintenance.ts @@ -1,6 +1,5 @@ import { getUnixTime, addMinutes } from "date-fns"; import { getUTCDate } from "../helpers/DateUtil"; -import MoiraApi from "../Api/MoiraApi"; export enum Maintenance { off = "off", @@ -15,7 +14,6 @@ export enum Maintenance { } export const MaintenanceList = [ - Maintenance.off, Maintenance.quarterHour, Maintenance.oneHour, Maintenance.threeHours, @@ -24,6 +22,7 @@ export const MaintenanceList = [ Maintenance.oneWeek, Maintenance.twoWeeks, Maintenance.oneMonth, + Maintenance.off, ]; const MaintenanceTimes = { @@ -64,22 +63,3 @@ export function calculateMaintenanceTime(maintenance: Maintenance): number { ? getUnixTime(addMinutes(getUTCDate(), maintenanceTime)) : maintenanceTime; } - -export async function setMetricMaintenance( - moiraApi: MoiraApi, - triggerId: string, - metric: string, - maintenance: number -): Promise<void> { - await moiraApi.setMaintenance(triggerId, { - metrics: { [metric]: maintenance }, - }); -} - -export async function setTriggerMaintenance( - moiraApi: MoiraApi, - triggerId: string, - maintenance: number -): Promise<void> { - await moiraApi.setMaintenance(triggerId, { trigger: maintenance }); -} diff --git a/src/Domain/Subscription.ts b/src/Domain/Subscription.ts index 97ae42f65..444f6cb2d 100644 --- a/src/Domain/Subscription.ts +++ b/src/Domain/Subscription.ts @@ -17,3 +17,9 @@ export type Subscription = { theme: "light" | "dark"; }; }; + +export type SubscriptionCreateInfo = Omit<Subscription, "id" | "user" | "any_tags"> & { + id?: string; + user?: string; + any_tags: boolean; +}; diff --git a/src/Domain/Tag.ts b/src/Domain/Tag.ts index 20268f7ef..8cde198e9 100644 --- a/src/Domain/Tag.ts +++ b/src/Domain/Tag.ts @@ -8,3 +8,11 @@ export type TagStat = { subscriptions: Array<Subscription>; triggers: Array<string>; }; + +export type TagList = { + list: Array<string>; +}; + +export type TagStatList = { + list: Array<TagStat>; +}; diff --git a/src/PrivateRoutes/AdminRoute.tsx b/src/PrivateRoutes/AdminRoute.tsx index c231a14a9..0dbe975ba 100644 --- a/src/PrivateRoutes/AdminRoute.tsx +++ b/src/PrivateRoutes/AdminRoute.tsx @@ -1,6 +1,5 @@ import React, { ComponentType } from "react"; import { Route, Redirect, RouteProps } from "react-router-dom"; -import type { TPatternListContainerProps } from "../Containers/PatternListContainer"; import { getPagePath } from "../Domain/Global"; import { useGetUserQuery } from "../services/UserApi"; import { EUserRoles } from "../Domain/User"; @@ -12,7 +11,7 @@ import styles from "../../local_modules/styles/mixins.less"; const cn = classNames.bind(styles); type PrivateRouteProps = RouteProps & { - component: ComponentType<TPatternListContainerProps | object>; + component: ComponentType<object>; exact?: boolean; path: string; }; diff --git a/src/Providers/Providers.tsx b/src/Providers/Providers.tsx index d0c853a2e..3225c3879 100644 --- a/src/Providers/Providers.tsx +++ b/src/Providers/Providers.tsx @@ -1,19 +1,13 @@ import React from "react"; -import MoiraApi from "../Api/MoiraApi"; -import { ApiProvider } from "../Api/MoiraApiInjection"; import { Provider } from "react-redux"; import { LocaleContext } from "@skbkontur/react-ui/lib/locale/LocaleContext"; import { LangCodes } from "@skbkontur/react-ui/lib/locale"; import { store } from "../store/store"; -const moiraApi = new MoiraApi("/api"); - export const Providers = ({ children }: { children: React.ReactNode }) => { return ( <LocaleContext.Provider value={{ langCode: LangCodes.en_GB }}> - <ApiProvider value={moiraApi}> - <Provider store={store}>{children}</Provider> - </ApiProvider> + <Provider store={store}>{children}</Provider> </LocaleContext.Provider> ); }; diff --git a/src/desktop.bundle.tsx b/src/desktop.bundle.tsx index 8c603df38..0b37773f4 100644 --- a/src/desktop.bundle.tsx +++ b/src/desktop.bundle.tsx @@ -1,7 +1,7 @@ import React, { ComponentType } from "react"; import { Switch, Route } from "react-router-dom"; import { hot } from "react-hot-loader/root"; -import HeaderContainer from "./Containers/HeaderContainer"; +import { HeaderContainer } from "./Containers/HeaderContainer"; import Footer from "./Components/Footer/Footer"; import TriggerEditContainer from "./Containers/TriggerEditContainer"; import TriggerDuplicateContainer from "./Containers/TriggerDuplicateContainer"; @@ -33,9 +33,7 @@ const cn = classNames.bind(styles); type ResponsiveRouteProps = { exact?: boolean; path: string; - container: - | ComponentType<Omit<TriggerListProps, "moiraApi">> - | ComponentType<Omit<TriggerProps, "moiraApi">>; + container: ComponentType<TriggerListProps> | ComponentType<TriggerProps>; view: ComponentType<TriggerListDesktopProps> | ComponentType<TriggerDesktopProps>; }; diff --git a/src/hooks/useCreateSubscription.tsx b/src/hooks/useCreateSubscription.tsx index 1f3c71564..a8b2130dc 100644 --- a/src/hooks/useCreateSubscription.tsx +++ b/src/hooks/useCreateSubscription.tsx @@ -5,7 +5,7 @@ import { useTestSubscriptionMutation, } from "../services/SubscriptionsApi"; import { useCreateTeamSubscriptionMutation } from "../services/TeamsApi"; -import type { SubscriptionCreateInfo } from "../Api/MoiraApi"; +import type { SubscriptionCreateInfo } from "../Domain/Subscription"; import { useAppDispatch } from "../store/hooks"; import { BaseApi } from "../services/BaseApi"; diff --git a/src/hooks/useLoadNotificationsData.tsx b/src/hooks/useLoadNotificationsData.tsx index c3ef0f4d0..566ad8aa6 100644 --- a/src/hooks/useLoadNotificationsData.tsx +++ b/src/hooks/useLoadNotificationsData.tsx @@ -1,29 +1,14 @@ -import { useDispatch } from "react-redux"; -import { toggleLoading } from "../store/Reducers/UIReducer.slice"; -import MoiraApi from "../Api/MoiraApi"; -import { - setNotificationList, - setNotifierEnabled, -} from "../store/Reducers/NotificationListContainerReducer.slice"; -import { setError } from "../store/Reducers/UIReducer.slice"; import MoiraServiceStates from "../Domain/MoiraServiceStates"; +import { useGetNotificationsQuery } from "../services/NotificationsApi"; +import { useGetNotifierStateQuery } from "../services/NotifierApi"; -export const useLoadNotificationsData = (moiraApi: MoiraApi) => { - const dispatch = useDispatch(); +export const useLoadNotificationsData = () => { + const { data: notifier } = useGetNotifierStateQuery(); + const { data: notifications } = useGetNotificationsQuery(); - const loadNotificationsData = async () => { - dispatch(toggleLoading(true)); - try { - const { list } = await moiraApi.getNotificationList(); - dispatch(setNotificationList(list)); - const notifier = await moiraApi.getNotifierState(); - dispatch(setNotifierEnabled(notifier?.state === MoiraServiceStates.OK)); - } catch (error) { - dispatch(setError(error.message)); - } finally { - dispatch(toggleLoading(false)); - } + return { + notifierEnabled: notifier?.state === MoiraServiceStates.OK, + notificationList: notifications?.list, + notificationAmount: notifications?.total ?? 0, }; - - return { loadNotificationsData }; }; diff --git a/src/hooks/useSaveTrigger.tsx b/src/hooks/useSaveTrigger.tsx index 4bca87e28..11b83c911 100644 --- a/src/hooks/useSaveTrigger.tsx +++ b/src/hooks/useSaveTrigger.tsx @@ -1,34 +1,30 @@ -import { Action, setError, setIsLoading } from "./useTriggerFormContainerReducer"; import { getPageLink } from "../Domain/Global"; -import MoiraApi from "../Api/MoiraApi"; -import { Dispatch } from "react"; import { History } from "history"; import { Trigger, triggerClientToPayload } from "../Domain/Trigger"; +import { useAddTriggerMutation, useSetTriggerMutation } from "../services/TriggerApi"; -export const useSaveTrigger = ( - moiraApi: MoiraApi, - dispatch: Dispatch<Action>, - history: History<unknown> -) => { - return async (trigger?: Trigger | Partial<Trigger>) => { +export const useSaveTrigger = (history: History<unknown>) => { + const [setTrigger] = useSetTriggerMutation(); + const [addTrigger] = useAddTriggerMutation(); + + return async (trigger?: Partial<Trigger>) => { if (!trigger) { return; } const triggerPayload = triggerClientToPayload(trigger); - dispatch(setIsLoading(true)); + const triggerID = triggerPayload.id; + const action = triggerID + ? () => setTrigger({ id: triggerID, data: triggerPayload }).unwrap() + : () => addTrigger(triggerPayload).unwrap(); + try { - const triggerID = triggerPayload.id; - const action = triggerID - ? () => moiraApi.setTrigger(triggerID, triggerPayload) - : () => moiraApi.addTrigger(triggerPayload); - const { id } = await action(); + const result = await action(); + const { id } = result; history.push(getPageLink("trigger", id)); - } catch (error) { - dispatch(setError(error.message)); - } finally { - dispatch(setIsLoading(false)); + } catch { + return; } }; }; diff --git a/src/hooks/useTriggerFormContainerReducer.tsx b/src/hooks/useTriggerFormContainerReducer.tsx deleted file mode 100644 index d50a85d18..000000000 --- a/src/hooks/useTriggerFormContainerReducer.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useReducer } from "react"; -import { ValidateTargetsResult } from "../Domain/Trigger"; - -export interface State { - isLoading: boolean; - isSaveModalVisible: boolean; - isSaveButtonDisabled: boolean; - validationResult?: ValidateTargetsResult; - error?: string | null; -} - -export enum ActionType { - setIsLoading = "setIsLoading", - setIsSaveButtonDisabled = "setIsSaveButtonDisabled", - setIsSaveModalVisible = "setIsSaveModalVisible", - setValidationResult = "setValidationResult", - setError = "setError", -} - -export const setIsLoading = (payload: boolean): Action => ({ - type: ActionType.setIsLoading, - payload, -}); -export const setIsSaveButtonDisabled = (payload: boolean): Action => ({ - type: ActionType.setIsSaveButtonDisabled, - payload, -}); -export const setIsSaveModalVisible = (payload: boolean): Action => ({ - type: ActionType.setIsSaveModalVisible, - payload, -}); -export const setValidationResult = (payload: ValidateTargetsResult): Action => ({ - type: ActionType.setValidationResult, - payload, -}); -export const setError = (payload: string | null): Action => ({ - type: ActionType.setError, - payload, -}); - -export type Action = - | { - type: ActionType.setIsLoading; - payload: boolean; - } - | { - type: ActionType.setIsSaveButtonDisabled; - payload: boolean; - } - | { - type: ActionType.setIsSaveModalVisible; - payload: boolean; - } - | { - type: ActionType.setValidationResult; - payload: ValidateTargetsResult; - } - | { - type: ActionType.setError; - payload: string | null; - }; - -const initialState: State = { - isLoading: false, - isSaveModalVisible: false, - isSaveButtonDisabled: false, - validationResult: undefined, - error: null, -}; - -const reducer = (state: State, action: Action) => { - switch (action.type) { - case ActionType.setIsLoading: - return { ...state, isLoading: action.payload }; - case ActionType.setIsSaveButtonDisabled: - return { ...state, isSaveButtonDisabled: action.payload }; - case ActionType.setIsSaveModalVisible: - return { ...state, isSaveModalVisible: action.payload }; - case ActionType.setValidationResult: - return { ...state, validationResult: action.payload }; - case ActionType.setError: - return { ...state, error: action.payload }; - default: - throw new Error(`Unknown action: ${JSON.stringify(action)}`); - } -}; - -export const useTriggerFormContainerReducer = () => useReducer(reducer, initialState); diff --git a/src/hooks/useValidateTarget.tsx b/src/hooks/useValidateTarget.tsx index 0e9ceb1e2..88795bcf1 100644 --- a/src/hooks/useValidateTarget.tsx +++ b/src/hooks/useValidateTarget.tsx @@ -1,38 +1,33 @@ import { Dispatch } from "react"; -import MoiraApi from "../Api/MoiraApi"; import type { Trigger } from "../Domain/Trigger"; import { checkTriggerTarget, triggerClientToPayload, TriggerTargetProblemType, } from "../Domain/Trigger"; +import { useSaveTrigger } from "./useSaveTrigger"; +import { History } from "history"; +import { useValidateTargetMutation } from "../services/TriggerApi"; import { - Action, - setError, - setIsLoading, setIsSaveButtonDisabled, - setIsSaveModalVisible, setValidationResult, -} from "./useTriggerFormContainerReducer"; -import { useSaveTrigger } from "./useSaveTrigger"; -import { History } from "history"; + setIsSaveModalVisible, +} from "../store/Reducers/TriggerFormReducer.slice"; +import { Action } from "@reduxjs/toolkit"; -export const useValidateTarget = ( - moiraApi: MoiraApi, - dispatch: Dispatch<Action>, - history: History<unknown> -) => { - const saveTrigger = useSaveTrigger(moiraApi, dispatch, history); +export const useValidateTarget = (dispatch: Dispatch<Action>, history: History<unknown>) => { + const saveTrigger = useSaveTrigger(history); + const [validateTarget] = useValidateTargetMutation(); return async (trigger?: Trigger | Partial<Trigger>) => { if (!trigger) { return; } - dispatch(setIsLoading(true)); + const triggerPayload = triggerClientToPayload(trigger); + try { - const triggerPayload = triggerClientToPayload(trigger); - const validationResult = await moiraApi.validateTarget(triggerPayload); + const validationResult = await validateTarget(triggerPayload).unwrap(); const doAnyTargetsHaveError = validationResult.targets.some((target) => checkTriggerTarget(target, TriggerTargetProblemType.BAD) @@ -56,10 +51,8 @@ export const useValidateTarget = ( } await saveTrigger(triggerPayload); - } catch (error) { - dispatch(setError(error.message)); - } finally { - dispatch(setIsLoading(false)); + } catch { + return; } }; }; diff --git a/src/mobile.bundle.tsx b/src/mobile.bundle.tsx index 84eb6407e..e19f947ed 100644 --- a/src/mobile.bundle.tsx +++ b/src/mobile.bundle.tsx @@ -15,9 +15,7 @@ import { TeamSettingsPrivateRoute } from "./PrivateRoutes/TeamSettingsPrivateRou type ResponsiveRouteProps = { exact?: boolean; path: string; - container: - | ComponentType<Omit<TriggerListProps, "moiraApi">> - | ComponentType<Omit<TriggerProps, "moiraApi">>; + container: ComponentType<TriggerListProps> | ComponentType<TriggerProps>; view: ComponentType<TriggerListMobileProps> | ComponentType<TriggerMobileProps>; }; diff --git a/src/pages/trigger-list/trigger-list.desktop.tsx b/src/pages/trigger-list/trigger-list.desktop.tsx index 14aff3bb7..88c6205e0 100644 --- a/src/pages/trigger-list/trigger-list.desktop.tsx +++ b/src/pages/trigger-list/trigger-list.desktop.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement } from "react"; +import React from "react"; import { History } from "history"; import difference from "lodash/difference"; import { Paging } from "@skbkontur/react-ui/components/Paging"; @@ -23,98 +23,94 @@ export type TriggerListDesktopProps = { onChange: (update: TriggerListUpdate) => void; searchText: string; loading: boolean; - error?: string; + error?: string | null; onSetMetricMaintenance: (triggerId: string, metric: string, maintenance: number) => void; onRemoveMetric: (triggerId: string, metric: string) => void; history: History; }; -export default class TriggerListDesktop extends React.Component<TriggerListDesktopProps> { - public render(): ReactElement { - const { - selectedTags, - subscribedTags, - allTags, - onlyProblems, - triggers, - activePage, - pageCount, - onChange, - searchText, - loading, - error, - onSetMetricMaintenance, - onRemoveMetric, - history, - } = this.props; - - return ( - <Layout loading={loading} error={error}> - <LayoutPlate> - <RowStack verticalAlign="baseline" block gap={3}> - <Fill> - <SearchSelector - search={searchText} - allTags={this.props.allTags} - loading={this.props.loading} - selectedTokens={selectedTags} - subscribedTokens={difference(subscribedTags, selectedTags)} - remainingTokens={difference(allTags, selectedTags)} - onChange={this.handleChange} - onSearch={this.handleSearch} - /> - </Fill> - <Fit> - <Toggle - checked={onlyProblems} - onValueChange={(value: boolean) => - onChange({ onlyProblems: value }) - } - />{" "} - Only Problems - </Fit> - </RowStack> - </LayoutPlate> - <LayoutContent> - <ColumnStack block gap={6} horizontalAlign="stretch"> - <AddingButton to={getPageLink("triggerAdd")} /> - <TriggerList - searchMode={searchText !== ""} - items={triggers} - onChange={onSetMetricMaintenance} - onRemove={onRemoveMetric} - history={history} - /> - </ColumnStack> - </LayoutContent> - {pageCount > 1 && ( - <LayoutFooter> - <Paging - caption="Next page" - activePage={activePage} - pagesCount={pageCount} - onPageChange={this.handlePageChange} - withoutNavigationHint - /> - </LayoutFooter> - )} - </Layout> - ); - } - - handlePageChange = (page: number) => { - this.props.onChange({ page }); +const TriggerListDesktop: React.FC<TriggerListDesktopProps> = ({ + selectedTags, + subscribedTags, + allTags, + onlyProblems, + triggers, + activePage, + pageCount, + onChange, + searchText, + loading, + error, + onSetMetricMaintenance, + onRemoveMetric, + history, +}) => { + const handlePageChange = (page: number) => { + onChange({ page }); window.scrollTo({ top: 0, behavior: "smooth", }); }; - handleChange = (tags: string[], searchText: string): void => { - this.props.onChange({ tags, searchText }); + const handleChange = (tags: string[], searchText: string) => { + onChange({ tags, searchText }); }; - handleSearch = (searchText: string): void => { - this.props.onChange({ searchText }); + const handleSearch = (searchText: string) => { + onChange({ searchText }); }; -} + + return ( + <Layout loading={loading} error={error}> + <LayoutPlate> + <RowStack verticalAlign="baseline" block gap={3}> + <Fill> + <SearchSelector + search={searchText} + allTags={allTags} + loading={loading} + selectedTokens={selectedTags} + subscribedTokens={difference(subscribedTags, selectedTags)} + remainingTokens={difference(allTags, selectedTags)} + onChange={handleChange} + onSearch={handleSearch} + /> + </Fill> + <Fit> + <Toggle + checked={onlyProblems} + onValueChange={(value: boolean) => onChange({ onlyProblems: value })} + />{" "} + Only Problems + </Fit> + </RowStack> + </LayoutPlate> + <LayoutContent> + <ColumnStack block gap={6} horizontalAlign="stretch"> + <AddingButton to={getPageLink("triggerAdd")} /> + <TriggerList + searchMode={!!searchText} + items={triggers} + onChange={onSetMetricMaintenance} + onRemove={onRemoveMetric} + history={history} + /> + </ColumnStack> + </LayoutContent> + {pageCount > 1 && ( + <LayoutFooter> + <Paging + caption="Next page" + activePage={activePage} + pagesCount={pageCount} + onPageChange={handlePageChange} + withoutNavigationHint + /> + </LayoutFooter> + )} + </Layout> + ); +}; + +export default TriggerListDesktop; diff --git a/src/pages/trigger-list/trigger-list.tsx b/src/pages/trigger-list/trigger-list.tsx index e20f9ba36..568e801bd 100644 --- a/src/pages/trigger-list/trigger-list.tsx +++ b/src/pages/trigger-list/trigger-list.tsx @@ -1,22 +1,27 @@ -import React, { ComponentType, ReactElement } from "react"; +import React, { useEffect, useState, ComponentType } from "react"; import { RouteComponentProps } from "react-router-dom"; -import isEqual from "lodash/isEqual"; import flattenDeep from "lodash/flattenDeep"; import uniq from "lodash/uniq"; -import queryString from "query-string"; -import { withMoiraApi } from "../../Api/MoiraApiInjection"; -import { Trigger, TriggerList } from "../../Domain/Trigger"; +import qs from "qs"; +import { TriggerList } from "../../Domain/Trigger"; import { MoiraUrlParams } from "../../Domain/MoiraUrlParams"; -import { setMetricMaintenance } from "../../Domain/Maintenance"; import transformPageFromHumanToProgrammer from "../../logic/transformPageFromHumanToProgrammer"; import { TriggerListMobileProps } from "./trigger-list.mobile"; import { TriggerListDesktopProps } from "./trigger-list.desktop"; -import MoiraApi from "../../Api/MoiraApi"; import { clearInput } from "../../helpers/common"; import { setDocumentTitle } from "../../helpers/setDocumentTitle"; +import { useAppSelector } from "../../store/hooks"; +import { UIState } from "../../store/selectors"; +import { useGetUserSettingsQuery } from "../../services/UserApi"; +import { useGetTagsQuery } from "../../services/TagsApi"; +import { + useDeleteMetricMutation, + useGetTriggerListQuery, + useSetMetricsMaintenanceMutation, +} from "../../services/TriggerApi"; export type TriggerListUpdate = { - tags?: Array<string>; + tags?: string[]; page?: number; searchText?: string; onlyProblems?: boolean; @@ -24,188 +29,148 @@ export type TriggerListUpdate = { export type TriggerListProps = RouteComponentProps & { view: ComponentType<TriggerListDesktopProps | TriggerListMobileProps>; - moiraApi: MoiraApi; }; -type State = { - loading: boolean; - error?: string; - subscribedTags: string[]; - allTags: string[]; - triggers: Trigger[]; - activePage: number; - pageCount: number; +const parseLocationSearch = (search: string): MoiraUrlParams => { + const START_PAGE = 1; + const { page, tags, onlyProblems, searchText } = qs.parse(search, { + ignoreQueryPrefix: true, + }); + + return { + page: + Number.isNaN(Number(page)) || typeof page !== "string" + ? START_PAGE + : Math.abs(parseInt(page, 10)), + tags: Array.isArray(tags) ? tags.map((value) => value.toString()) : [], + onlyProblems: onlyProblems === "false" ? false : Boolean(onlyProblems), + searchText: clearInput(typeof searchText === "string" ? searchText : ""), + }; }; -class TriggerListPage extends React.Component<TriggerListProps, State> { - state: State = { - loading: true, - subscribedTags: [], - allTags: [], - triggers: [], - activePage: 1, - pageCount: 1, - }; +const changeLocationSearch = ( + history: RouteComponentProps["history"], + locationSearch: MoiraUrlParams, + update: TriggerListUpdate +) => { + const settings = { ...locationSearch, ...update }; + localStorage.setItem("moiraSettings", JSON.stringify({ ...settings, searchText: "" })); + history.push(`?${qs.stringify(settings, { arrayFormat: "indices", encode: true })}`); +}; - componentDidMount() { - setDocumentTitle("Triggers"); - this.loadData(); +const loadLocalSettingsAndRedirectIfNeed = ( + history: RouteComponentProps["history"], + locationSearch: MoiraUrlParams, + tags: Array<string>, + onlyProblems: boolean +) => { + const localDataString = localStorage.getItem("moiraSettings"); + const { tags: localTags, onlyProblems: localOnlyProblems }: TriggerListUpdate = + typeof localDataString === "string" ? JSON.parse(localDataString) : {}; + + const searchToUpdate: TriggerListUpdate = {}; + const isTagParamEnabled = tags.length === 0 && localTags?.length; + const isOnlyProblemsParamEnabled = !onlyProblems && localOnlyProblems; + + if (isTagParamEnabled) { + searchToUpdate.tags = localTags; } - - componentDidUpdate({ location: prevLocation }: TriggerListProps): void { - const { location: currentLocation } = this.props; - if (!isEqual(prevLocation, currentLocation)) { - this.loadData(); - } + if (isOnlyProblemsParamEnabled) { + searchToUpdate.onlyProblems = localOnlyProblems; } - - static parseLocationSearch(search: string): MoiraUrlParams { - const START_PAGE = 1; - const { page, tags, onlyProblems, searchText } = queryString.parse(search, { - arrayFormat: "index", - }); - - return { - page: - Number.isNaN(Number(page)) || typeof page !== "string" - ? START_PAGE - : Math.abs(parseInt(page, 10)), - tags: Array.isArray(tags) ? tags.map((value) => value.toString()) : [], - onlyProblems: onlyProblems === "false" ? false : Boolean(onlyProblems), - searchText: clearInput(searchText || ""), - }; + if (isTagParamEnabled || isOnlyProblemsParamEnabled) { + changeLocationSearch(history, locationSearch, searchToUpdate); + return true; } + return false; +}; - public render(): ReactElement { - const { location } = this.props; - const locationSearch = TriggerListPage.parseLocationSearch(location.search); - const { onlyProblems, tags, searchText } = locationSearch; - - const { - loading, - error, - subscribedTags, - allTags, - triggers, - activePage, - pageCount, - } = this.state; - const { view: TriggerListView } = this.props; - - return ( - <TriggerListView - searchText={searchText} - selectedTags={tags} - subscribedTags={subscribedTags} - allTags={allTags} - onlyProblems={onlyProblems} - triggers={triggers} - activePage={activePage} - pageCount={pageCount} - loading={loading} - error={error} - onChange={this.changeLocationSearch} - onSetMetricMaintenance={this.setMetricMaintenance} - onRemoveMetric={this.removeMetric} - history={this.props.history} - /> - ); +const checkPageAndRedirectIfNeeded = ( + triggerList: TriggerList, + page: number, + changeLocationSearch: (update: TriggerListUpdate) => void +) => { + const pages = Math.ceil(triggerList.total / triggerList.size); + if (page > pages && triggerList.total !== 0) { + changeLocationSearch({ page: pages || 1 }); + return true; } + return false; +}; - private async loadData() { - const { location, moiraApi } = this.props; - const locationSearch = TriggerListPage.parseLocationSearch(location.search); - const redirected = this.loadLocalSettingsAndRedirectIfNeed( - locationSearch.tags, - locationSearch.onlyProblems - ); +const TriggerListPage: React.FC<TriggerListProps> = ({ + view: TriggerListView, + location, + history, +}) => { + const { isLoading, error } = useAppSelector(UIState); + const [activePage, setActivePage] = useState(1); + const [pageCount, setPageCount] = useState(1); - if (redirected) return; - - try { - const [settings, triggers, tags] = await Promise.all([ - moiraApi.getSettings(), - moiraApi.getTriggerList( - transformPageFromHumanToProgrammer(locationSearch.page), - locationSearch.onlyProblems, - locationSearch.tags, - locationSearch.searchText - ), - moiraApi.getTagList(), - ]); - - if (this.checkPageAndRedirectIfNeed(triggers, locationSearch.page)) return; - - this.setState({ - subscribedTags: uniq(flattenDeep(settings.subscriptions.map((item) => item.tags))), - allTags: tags.list, - // TODO: check getTriggerList always return trigger list? - triggers: triggers.list ?? [], - activePage: locationSearch.page, - pageCount: Math.ceil(triggers.total / triggers.size), - loading: false, - }); - } catch (error) { - this.setState({ loading: false, error: error.message }); - } - } + const { data: settings } = useGetUserSettingsQuery(); + const { data: tags } = useGetTagsQuery(); + const locationSearch = parseLocationSearch(location.search); - checkPageAndRedirectIfNeed(triggers: TriggerList, page: number): boolean { - const pages = Math.ceil(triggers.total / triggers.size); - if (page > pages && triggers.total !== 0) { - const rightLastPage = pages || 1; - this.changeLocationSearch({ page: rightLastPage }); - return true; - } - return false; - } + const [setMetricMaintenance] = useSetMetricsMaintenanceMutation(); + const [deleteMetric] = useDeleteMetricMutation(); - loadLocalSettingsAndRedirectIfNeed(tags: Array<string>, onlyProblems: boolean) { - const localDataString = localStorage.getItem("moiraSettings"); - const { tags: localTags, onlyProblems: localOnlyProblems }: TriggerListUpdate = - typeof localDataString === "string" ? JSON.parse(localDataString) : {}; - - const searchToUpdate: TriggerListUpdate = {}; - const isTagParamEnabled = tags.length === 0 && localTags?.length; - const isOnlyProblemsParamEnabled = !onlyProblems && localOnlyProblems; - - if (isTagParamEnabled) { - searchToUpdate.tags = localTags; - } - if (isOnlyProblemsParamEnabled) { - searchToUpdate.onlyProblems = localOnlyProblems; - } - if (isTagParamEnabled || isOnlyProblemsParamEnabled) { - this.changeLocationSearch(searchToUpdate); - return true; - } - return false; - } + const { data: triggerList } = useGetTriggerListQuery({ + page: transformPageFromHumanToProgrammer(locationSearch.page), + onlyProblems: locationSearch.onlyProblems, + tags: locationSearch.tags, + searchText: locationSearch.searchText, + }); - changeLocationSearch = (update: TriggerListUpdate) => { - const { location, history } = this.props; - const locationSearch = TriggerListPage.parseLocationSearch(location.search); - const settings = { ...locationSearch, ...update }; - localStorage.setItem("moiraSettings", JSON.stringify({ ...settings, searchText: "" })); - history.push( - `?${queryString.stringify(settings, { - arrayFormat: "index", - encode: true, - })}` - ); - }; + const subscribedTags = uniq(flattenDeep(settings?.subscriptions.map((item) => item.tags))); - setMetricMaintenance = async (triggerId: string, metric: string, maintenance: number) => { - this.setState({ loading: true }); - const { moiraApi } = this.props; - setMetricMaintenance(moiraApi, triggerId, metric, maintenance); - }; + useEffect(() => { + setDocumentTitle("Triggers"); + const redirected = loadLocalSettingsAndRedirectIfNeed( + history, + locationSearch, + locationSearch.tags, + locationSearch.onlyProblems + ); - removeMetric = async (triggerId: string, metric: string): Promise<void> => { - this.setState({ loading: true }); - const { moiraApi } = this.props; - await moiraApi.delMetric(triggerId, metric); - }; -} + if (redirected || !triggerList) return; + + if ( + checkPageAndRedirectIfNeeded(triggerList, locationSearch.page, (update) => + changeLocationSearch(history, locationSearch, update) + ) + ) + return; + + setActivePage(locationSearch.page); + setPageCount(Math.ceil(triggerList.total / triggerList.size)); + }, [triggerList]); + + return ( + <TriggerListView + searchText={locationSearch.searchText} + selectedTags={locationSearch.tags} + subscribedTags={subscribedTags} + allTags={tags ?? []} + onlyProblems={locationSearch.onlyProblems} + triggers={triggerList?.list ?? []} + activePage={activePage} + pageCount={pageCount} + loading={isLoading} + error={error} + onChange={(update) => changeLocationSearch(history, locationSearch, update)} + onSetMetricMaintenance={(triggerId: string, metric: string, maintenance: number) => + setMetricMaintenance({ + triggerId, + metrics: { [metric]: maintenance }, + tagsToInvalidate: ["TriggerList"], + }) + } + onRemoveMetric={(triggerId, metric) => + deleteMetric({ triggerId, metric, tagsToInvalidate: ["TriggerList"] }) + } + history={history} + /> + ); +}; -export default withMoiraApi(TriggerListPage); +export default TriggerListPage; diff --git a/src/pages/trigger/trigger.tsx b/src/pages/trigger/trigger.tsx index a46e6cf1b..367db3547 100644 --- a/src/pages/trigger/trigger.tsx +++ b/src/pages/trigger/trigger.tsx @@ -44,12 +44,21 @@ const TriggerPage: React.FC<TriggerProps> = ({ view: TriggerView }) => { const handleDisableThrottling = async () => await deleteTriggerThrottling(triggerId); const handleSetTriggerMaintenance = async (maintenance: number) => - await setTriggerMaintenance({ triggerId, maintenance }); + await setTriggerMaintenance({ + triggerId, + maintenance, + tagsToInvalidate: ["TriggerState", "TriggerList"], + }); const handleSetMetricMaintenance = async (metric: string, maintenance: number) => - await setMetricMaintenance({ triggerId, metrics: { [metric]: maintenance } }); + await setMetricMaintenance({ + triggerId, + metrics: { [metric]: maintenance }, + tagsToInvalidate: ["TriggerState", "TriggerList"], + }); - const handleRemoveMetric = async (metric: string) => await deleteMetric({ triggerId, metric }); + const handleRemoveMetric = async (metric: string) => + await deleteMetric({ triggerId, metric, tagsToInvalidate: ["TriggerState"] }); const handleRemoveNoDataMetric = async () => await deleteNoDataMetrics(triggerId); diff --git a/src/services/BaseApi.ts b/src/services/BaseApi.ts index 56bdf69cd..a970fc808 100644 --- a/src/services/BaseApi.ts +++ b/src/services/BaseApi.ts @@ -71,6 +71,9 @@ export type TApiInvalidateTags = | "Team" | "TriggerState" | "Trigger" + | "Notifications" + | "Patterns" + | "TriggerList" | "AllTeams"; export const BaseApi = createApi({ @@ -86,6 +89,9 @@ export const BaseApi = createApi({ "Team", "TriggerState", "Trigger", + "Notifications", + "Patterns", + "TriggerList", "AllTeams", ], baseQuery: customFetchBaseQuery, diff --git a/src/services/NotificationsApi.ts b/src/services/NotificationsApi.ts new file mode 100644 index 000000000..f80eb8056 --- /dev/null +++ b/src/services/NotificationsApi.ts @@ -0,0 +1,56 @@ +import { NotificationList } from "../Domain/Notification"; +import { BaseApi, CustomBaseQueryArgs } from "./BaseApi"; + +export const NotificationsApi = BaseApi.injectEndpoints({ + endpoints: (builder) => ({ + getNotifications: builder.query<NotificationList, CustomBaseQueryArgs | void>({ + query: () => ({ + url: "notification?start=0&end=-1", + method: "GET", + credentials: "same-origin", + }), + providesTags: ["Notifications"], + }), + + deleteNotification: builder.mutation<void, CustomBaseQueryArgs<{ id: string }>>({ + query: ({ id }) => ({ + url: `notification?id=${encodeURIComponent(id)}`, + credentials: "same-origin", + method: "DELETE", + }), + invalidatesTags: (_result, error) => { + if (error) { + return []; + } + return ["Notifications"]; + }, + }), + deleteAllNotifications: builder.mutation<void, CustomBaseQueryArgs | void>({ + query: () => ({ + url: "notification/all", + credentials: "same-origin", + method: "DELETE", + }), + invalidatesTags: (_result, error) => { + if (error) { + return []; + } + return ["Notifications"]; + }, + }), + deleteAllNotificationEvents: builder.mutation<void, CustomBaseQueryArgs | void>({ + query: () => ({ + url: "event/all", + credentials: "same-origin", + method: "DELETE", + }), + }), + }), +}); + +export const { + useGetNotificationsQuery, + useDeleteNotificationMutation, + useDeleteAllNotificationsMutation, + useDeleteAllNotificationEventsMutation, +} = NotificationsApi; diff --git a/src/services/NotifierApi.ts b/src/services/NotifierApi.ts new file mode 100644 index 000000000..a4d2d60c1 --- /dev/null +++ b/src/services/NotifierApi.ts @@ -0,0 +1,30 @@ +import { NotifierState } from "../Domain/MoiraServiceStates"; +import { BaseApi, CustomBaseQueryArgs } from "./BaseApi"; + +export const NotifierApi = BaseApi.injectEndpoints({ + endpoints: (builder) => ({ + getNotifierState: builder.query<NotifierState, CustomBaseQueryArgs | void>({ + query: () => ({ + url: "health/notifier", + method: "GET", + credentials: "same-origin", + }), + }), + setNotifierState: builder.mutation<NotifierState, CustomBaseQueryArgs<NotifierState>>({ + query: (state) => ({ + url: `/health/notifier`, + method: "PUT", + credentials: "same-origin", + body: JSON.stringify(state), + }), + invalidatesTags: (_result, error) => { + if (error) { + return []; + } + return ["Notifications"]; + }, + }), + }), +}); + +export const { useGetNotifierStateQuery, useSetNotifierStateMutation } = NotifierApi; diff --git a/src/services/PatternsApi.ts b/src/services/PatternsApi.ts new file mode 100644 index 000000000..de76a95bd --- /dev/null +++ b/src/services/PatternsApi.ts @@ -0,0 +1,32 @@ +import { Pattern, PatternList } from "../Domain/Pattern"; +import { BaseApi, CustomBaseQueryArgs } from "./BaseApi"; + +export const PatternsApi = BaseApi.injectEndpoints({ + endpoints: (builder) => ({ + getPatterns: builder.query<Array<Pattern>, CustomBaseQueryArgs | void>({ + query: () => ({ + url: "pattern", + method: "GET", + credentials: "same-origin", + }), + transformResponse: (response: PatternList) => response.list, + providesTags: ["Patterns"], + }), + + deletePattern: builder.mutation<void, CustomBaseQueryArgs<string>>({ + query: (pattern) => ({ + url: `pattern/${encodeURIComponent(pattern)}`, + credentials: "same-origin", + method: "DELETE", + }), + invalidatesTags: (_result, error) => { + if (error) { + return []; + } + return ["Patterns"]; + }, + }), + }), +}); + +export const { useGetPatternsQuery, useDeletePatternMutation } = PatternsApi; diff --git a/src/services/SubscriptionsApi.ts b/src/services/SubscriptionsApi.ts index 462d69bad..a137e1f18 100644 --- a/src/services/SubscriptionsApi.ts +++ b/src/services/SubscriptionsApi.ts @@ -1,4 +1,4 @@ -import { SubscriptionCreateInfo } from "../Api/MoiraApi"; +import { SubscriptionCreateInfo } from "../Domain/Subscription"; import { Subscription } from "../Domain/Subscription"; import { BaseApi, CustomBaseQueryArgs, TApiInvalidateTags } from "./BaseApi"; diff --git a/src/services/TagsApi.ts b/src/services/TagsApi.ts index 68391a7bc..405b9b74a 100644 --- a/src/services/TagsApi.ts +++ b/src/services/TagsApi.ts @@ -1,11 +1,11 @@ import { BaseApi, CustomBaseQueryArgs } from "./BaseApi"; -import { TagList, TagStatList } from "../Api/MoiraApi"; -import { TagStat } from "../Domain/Tag"; +import { TagList, TagStatList, TagStat } from "../Domain/Tag"; export const TagsApi = BaseApi.injectEndpoints({ endpoints: (builder) => ({ - getTags: builder.query<TagList, CustomBaseQueryArgs | void>({ + getTags: builder.query<string[], CustomBaseQueryArgs | void>({ query: () => ({ url: "tag", method: "GET", credentials: "same-origin" }), + transformResponse: (response: TagList) => response.list, }), getTagStats: builder.query<Array<TagStat>, CustomBaseQueryArgs | void>({ query: () => ({ url: "tag/stats", method: "GET", credentials: "same-origin" }), diff --git a/src/services/TeamsApi.ts b/src/services/TeamsApi.ts index 21fbccb18..c8f1444b4 100644 --- a/src/services/TeamsApi.ts +++ b/src/services/TeamsApi.ts @@ -1,4 +1,4 @@ -import { SubscriptionCreateInfo } from "../Api/MoiraApi"; +import { SubscriptionCreateInfo } from "../Domain/Subscription"; import { Contact, TeamContactCreateInfo } from "../Domain/Contact"; import { Settings } from "../Domain/Settings"; import { Subscription } from "../Domain/Subscription"; diff --git a/src/services/TriggerApi.ts b/src/services/TriggerApi.ts index 980ae3d2a..513dea8b2 100644 --- a/src/services/TriggerApi.ts +++ b/src/services/TriggerApi.ts @@ -1,9 +1,11 @@ import { EventList } from "../Domain/Event"; import { Status } from "../Domain/Status"; -import { Trigger, TriggerState } from "../Domain/Trigger"; -import { BaseApi, CustomBaseQueryArgs } from "./BaseApi"; +import { Trigger, TriggerList, TriggerState, ValidateTargetsResult } from "../Domain/Trigger"; +import { BaseApi, CustomBaseQueryArgs, TApiInvalidateTags } from "./BaseApi"; +import qs from "qs"; const eventHistoryPageSize = 100; +const triggerListPageSize = 20; export const TriggerApi = BaseApi.injectEndpoints({ endpoints: (builder) => ({ @@ -13,32 +15,26 @@ export const TriggerApi = BaseApi.injectEndpoints({ triggerId: string; page: number; states?: Status[]; - metric?: string | null; + metric?: string; from?: number | null; to?: number | null; }> >({ query: ({ triggerId, page, states, metric, from, to }) => { - const params = new URLSearchParams({ - p: String(page), - size: String(eventHistoryPageSize), - }); - - if (states?.length) { - params.append("states", states.join(",")); - } - if (metric) { - params.append("metric", metric); - } - if (from) { - params.append("from", String(from)); - } - if (to) { - params.append("to", String(to)); - } + const params = qs.stringify( + { + p: page, + size: eventHistoryPageSize, + states: states?.length ? states : null, + metric, + from, + to, + }, + { arrayFormat: "comma", skipNulls: true } + ); return { - url: `event/${encodeURIComponent(triggerId)}?${params.toString()}`, + url: `event/${encodeURIComponent(triggerId)}?${params}`, method: "GET", credentials: "same-origin", }; @@ -87,7 +83,11 @@ export const TriggerApi = BaseApi.injectEndpoints({ }), setTriggerMaintenance: builder.mutation< void, - CustomBaseQueryArgs<{ triggerId: string; maintenance: number }> + CustomBaseQueryArgs<{ + triggerId: string; + maintenance: number; + tagsToInvalidate?: TApiInvalidateTags[]; + }> >({ query: ({ triggerId, maintenance }) => ({ url: `trigger/${encodeURIComponent(triggerId)}/setMaintenance`, @@ -95,19 +95,19 @@ export const TriggerApi = BaseApi.injectEndpoints({ credentials: "same-origin", body: JSON.stringify({ trigger: maintenance }), }), - invalidatesTags: (_result, error) => { + invalidatesTags: (_result, error, { tagsToInvalidate = [] }) => { if (error) { return []; } - return ["TriggerState"]; + return tagsToInvalidate; }, }), - setMetricsMaintenance: builder.mutation< void, CustomBaseQueryArgs<{ triggerId: string; metrics: { [metric: string]: number }; + tagsToInvalidate?: TApiInvalidateTags[]; }> >({ query: ({ triggerId, metrics }) => ({ @@ -116,16 +116,20 @@ export const TriggerApi = BaseApi.injectEndpoints({ credentials: "same-origin", body: JSON.stringify({ metrics }), }), - invalidatesTags: (_result, error) => { + invalidatesTags: (_result, error, { tagsToInvalidate = [] }) => { if (error) { return []; } - return ["TriggerState"]; + return tagsToInvalidate; }, }), deleteMetric: builder.mutation< void, - CustomBaseQueryArgs<{ triggerId: string; metric: string }> + CustomBaseQueryArgs<{ + triggerId: string; + metric: string; + tagsToInvalidate?: TApiInvalidateTags[]; + }> >({ query: ({ triggerId, metric }) => ({ url: `trigger/${encodeURIComponent(triggerId)}/metrics?name=${encodeURIComponent( @@ -134,11 +138,11 @@ export const TriggerApi = BaseApi.injectEndpoints({ method: "DELETE", credentials: "same-origin", }), - invalidatesTags: (_result, error) => { + invalidatesTags: (_result, error, { tagsToInvalidate = [] }) => { if (error) { return []; } - return ["TriggerState"]; + return tagsToInvalidate; }, }), deleteTrigger: builder.mutation<void, CustomBaseQueryArgs<string>>({ @@ -147,6 +151,12 @@ export const TriggerApi = BaseApi.injectEndpoints({ method: "DELETE", credentials: "same-origin", }), + invalidatesTags: (_result, error) => { + if (error) { + return []; + } + return ["TriggerList"]; + }, }), deleteNoDataMetric: builder.mutation<void, CustomBaseQueryArgs<string>>({ query: (triggerId) => ({ @@ -161,6 +171,72 @@ export const TriggerApi = BaseApi.injectEndpoints({ return ["TriggerState"]; }, }), + validateTarget: builder.mutation< + ValidateTargetsResult, + CustomBaseQueryArgs<Partial<Trigger>> + >({ + query: (trigger) => ({ + url: "trigger/check", + method: "PUT", + body: JSON.stringify(trigger), + credentials: "same-origin", + }), + }), + setTrigger: builder.mutation< + { + [key: string]: string; + }, + CustomBaseQueryArgs<{ id: string; data: Partial<Trigger> }> + >({ + query: ({ id, data }) => ({ + url: `trigger/${encodeURIComponent(id)}`, + method: "PUT", + body: JSON.stringify(data), + credentials: "same-origin", + }), + }), + addTrigger: builder.mutation< + { + [key: string]: string; + }, + CustomBaseQueryArgs<Partial<Trigger>> + >({ + query: (data) => ({ + url: "trigger", + method: "PUT", + body: JSON.stringify(data), + credentials: "same-origin", + }), + }), + getTriggerList: builder.query< + TriggerList, + CustomBaseQueryArgs<{ + page: number; + onlyProblems: boolean; + tags?: Array<string>; + searchText?: string; + }> + >({ + query: ({ page, onlyProblems, tags = [], searchText = "" }) => { + const params = qs.stringify( + { + p: page, + size: triggerListPageSize, + tags, + onlyProblems, + text: searchText, + }, + { arrayFormat: "indices", skipNulls: true, encode: true } + ); + + return { + url: `/trigger/search?${params}`, + method: "GET", + credentials: "same-origin", + }; + }, + providesTags: ["TriggerList"], + }), }), }); @@ -174,4 +250,8 @@ export const { useGetTriggerStateQuery, useSetMetricsMaintenanceMutation, useSetTriggerMaintenanceMutation, + useValidateTargetMutation, + useSetTriggerMutation, + useAddTriggerMutation, + useGetTriggerListQuery, } = TriggerApi; diff --git a/src/store/Reducers/NotificationListContainerReducer.slice.ts b/src/store/Reducers/NotificationListContainerReducer.slice.ts deleted file mode 100644 index 76946710a..000000000 --- a/src/store/Reducers/NotificationListContainerReducer.slice.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { Notification } from "../../Domain/Notification"; - -interface INotificationListContainerState { - notificationList: Notification[]; - notifierEnabled: boolean; -} - -const initialState: INotificationListContainerState = { - notificationList: [], - notifierEnabled: true, -}; - -const NotificationSlice = createSlice({ - name: "notifications", - initialState, - reducers: { - setNotificationList: (state, action: PayloadAction<Notification[]>) => { - state.notificationList = action.payload; - }, - setNotifierEnabled: (state, action: PayloadAction<boolean>) => { - state.notifierEnabled = action.payload; - }, - deleteNotification: (state, action: PayloadAction<string>) => { - state.notificationList = state.notificationList.filter( - (item) => item.timestamp + item.contact.id + item.event.sub_id !== action.payload - ); - }, - deleteAllNotifications: (state) => { - state.notificationList = []; - }, - }, -}); - -export const { - setNotificationList, - setNotifierEnabled, - deleteNotification, - deleteAllNotifications, -} = NotificationSlice.actions; - -export default NotificationSlice.reducer; diff --git a/src/store/Reducers/SettingsContainerReducer.slice.ts b/src/store/Reducers/SettingsContainerReducer.slice.ts deleted file mode 100644 index 71e1fd2ab..000000000 --- a/src/store/Reducers/SettingsContainerReducer.slice.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import type { Settings } from "../../Domain/Settings"; -import { Team } from "../../Domain/Team"; -import { TagList } from "../../Api/MoiraApi"; - -interface TeamsAndTags { - login: string; - teams: Team[]; - tags: TagList; - team?: Team; -} - -export interface ISettingsContainerState { - teamsAndTags?: TeamsAndTags; - settings?: Settings; -} - -const initialState: ISettingsContainerState = { - teamsAndTags: undefined, - settings: undefined, -}; - -const SettingsSlice = createSlice({ - name: "settings", - initialState, - reducers: { - setTeamsAndTags: (state, action: PayloadAction<Partial<TeamsAndTags>>) => { - state.teamsAndTags = { ...state.teamsAndTags, ...(action.payload as TeamsAndTags) }; - }, - setSettings: (state, action: PayloadAction<Partial<Settings>>) => { - state.settings = { ...state.settings, ...(action.payload as Settings) }; - }, - }, -}); - -export const { setTeamsAndTags, setSettings } = SettingsSlice.actions; - -export default SettingsSlice.reducer; diff --git a/src/store/Reducers/TriggerFormReducer.slice.ts b/src/store/Reducers/TriggerFormReducer.slice.ts new file mode 100644 index 000000000..7110156b5 --- /dev/null +++ b/src/store/Reducers/TriggerFormReducer.slice.ts @@ -0,0 +1,38 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { ValidateTargetsResult } from "../../Domain/Trigger"; + +interface State { + isSaveModalVisible: boolean; + isSaveButtonDisabled: boolean; + validationResult?: ValidateTargetsResult; +} + +const initialState: State = { + isSaveModalVisible: false, + isSaveButtonDisabled: false, + validationResult: undefined, +}; + +export const triggerFormSlice = createSlice({ + name: "triggerForm", + initialState, + reducers: { + setIsSaveButtonDisabled: (state, action: PayloadAction<boolean>) => { + state.isSaveButtonDisabled = action.payload; + }, + setIsSaveModalVisible: (state, action: PayloadAction<boolean>) => { + state.isSaveModalVisible = action.payload; + }, + setValidationResult: (state, action: PayloadAction<ValidateTargetsResult>) => { + state.validationResult = action.payload; + }, + }, +}); + +export const { + setIsSaveButtonDisabled, + setIsSaveModalVisible, + setValidationResult, +} = triggerFormSlice.actions; + +export default triggerFormSlice.reducer; diff --git a/src/store/selectors.ts b/src/store/selectors.ts index 507026601..48a64f56b 100644 --- a/src/store/selectors.ts +++ b/src/store/selectors.ts @@ -1,5 +1,5 @@ import { RootState } from "./store"; -export const NotificationsState = (state: RootState) => state.NotificationListContainerReducer; export const UIState = (state: RootState) => state.UIReducer; export const ConfigState = (state: RootState) => state.ConfigReducer; +export const TriggerFormState = (state: RootState) => state.TriggerFormReducer; diff --git a/src/store/store.ts b/src/store/store.ts index f150b4040..1e869c9c0 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,16 +1,16 @@ import { configureStore } from "@reduxjs/toolkit"; -import NotificationListContainerReducer from "./Reducers/NotificationListContainerReducer.slice"; import UIReducer from "./Reducers/UIReducer.slice"; import { BaseApi } from "../services/BaseApi"; import ConfigReducer from "./Reducers/ConfigReducer.slice"; +import TriggerFormReducer from "./Reducers/TriggerFormReducer.slice"; import { rtkQueryErrorAndLoadingHandler } from "../services/rtkQueryErrorAndLoadingHandler"; export const store = configureStore({ reducer: { [BaseApi.reducerPath]: BaseApi.reducer, ConfigReducer, - NotificationListContainerReducer, UIReducer, + TriggerFormReducer, }, middleware: (getDefaultMiddleware) =>