diff --git a/.github/workflows/fe-e2e.yaml b/.github/workflows/fe-e2e.yaml index e7a9555cb0..82e483161d 100644 --- a/.github/workflows/fe-e2e.yaml +++ b/.github/workflows/fe-e2e.yaml @@ -20,7 +20,7 @@ jobs: with: node-version: 14 cache: "npm" - cache-dependency-path: './thirdeye-ui/package-lock.json' + cache-dependency-path: "./thirdeye-ui/package-lock.json" - name: Install dependencies run: | @@ -93,8 +93,8 @@ jobs: } ] }" ${{ secrets.SLACK_WEBHOOK }} - + - name: Stop local server if: always() run: | - kill $(lsof -t -i:7004); \ No newline at end of file + kill $(lsof -t -i:7004); diff --git a/thirdeye-ui/cypress/fixtures/example.json b/thirdeye-ui/cypress/fixtures/example.json new file mode 100644 index 0000000000..519902d71a --- /dev/null +++ b/thirdeye-ui/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/thirdeye-ui/cypress/support/commands.ts b/thirdeye-ui/cypress/support/commands.ts new file mode 100644 index 0000000000..258c083975 --- /dev/null +++ b/thirdeye-ui/cypress/support/commands.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2024 StarTree Inc + * + * Licensed under the StarTree Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.startree.ai/legal/startree-community-license + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OF ANY KIND, + * either express or implied. + * + * See the License for the specific language governing permissions and limitations under + * the License. + */ +// / +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// eslint-disable-next-line max-len +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } diff --git a/thirdeye-ui/cypress/support/e2e.ts b/thirdeye-ui/cypress/support/e2e.ts new file mode 100644 index 0000000000..571451cba1 --- /dev/null +++ b/thirdeye-ui/cypress/support/e2e.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2024 StarTree Inc + * + * Licensed under the StarTree Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.startree.ai/legal/startree-community-license + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OF ANY KIND, + * either express or implied. + * + * See the License for the specific language governing permissions and limitations under + * the License. + */ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import "./commands"; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/thirdeye-ui/e2e/pages/alert-detail.ts b/thirdeye-ui/e2e/pages/alert-detail.ts new file mode 100644 index 0000000000..87920f82f0 --- /dev/null +++ b/thirdeye-ui/e2e/pages/alert-detail.ts @@ -0,0 +1,280 @@ +/* + * Copyright 2024 StarTree Inc + * + * Licensed under the StarTree Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.startree.ai/legal/startree-community-license + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OF ANY KIND, + * either express or implied. + * + * See the License for the specific language governing permissions and limitations under + * the License. + */ +import { expect, Page } from "@playwright/test"; +import { BasePage } from "./base"; + +export class AlertDetailsPage extends BasePage { + readonly page: Page; + alertsApiResponseData: any; + anomaliesApiResponseData: any; + anomalyApiResponseData: any; + evaluateResponseData: any; + + constructor(page: Page) { + super(page); + this.page = page; + } + + async resolveApis() { + const [alertsApiResponse] = await Promise.all([ + this.page.waitForResponse( + (response) => + response.url().includes("/api/alerts") && + response.status() === 200 + ), + ]); + + this.alertsApiResponseData = await alertsApiResponse.json(); + } + + async resolveAnomaliesApis() { + const [anomaliesApiResponse] = await Promise.all([ + this.page.waitForResponse( + (response) => + response.url().includes("/api/anomalies") && + response.status() === 200 + ), + ]); + + this.anomaliesApiResponseData = await anomaliesApiResponse.json(); + } + + async resolveAnomalyApis() { + const [anomalyApiResponse] = await Promise.all([ + this.page.waitForResponse( + (response) => + response + .url() + .includes( + `/api/anomalies/${this.anomaliesApiResponseData[0].id}` + ) && response.status() === 200 + ), + ]); + + this.anomalyApiResponseData = await anomalyApiResponse.json(); + } + + async resolveInvestigateAnomalyPageApis() { + const [anomalyApiResponse] = await Promise.all([ + this.page.waitForResponse( + (response) => + response.url().includes(`/api/rca/metrics/heatmap`) && + response.status() === 200 + ), + this.page.waitForResponse( + (response) => + response.url().includes(`/api/rca/dim-analysis`) && + response.status() === 200 + ), + this.page.waitForResponse( + (response) => + response.url().includes(`/api/alerts/evaluate`) && + response.status() === 200 + ), + ]); + } + + async goToAlertDetailsPage() { + await this.page.goto("http://localhost:7004/#access_token=''"); + await this.page.waitForSelector("h4:has-text('StarTree ThirdEye')", { + timeout: 10000, + state: "visible", + }); + await this.page.goto("http://localhost:7004/alerts/all"); + } + + async checkHeader() { + await expect(this.page.locator("h4")).toHaveText("Alerts"); + } + + async checkAlertHeader() { + await expect(this.page.locator("h4")).toHaveText( + this.alertsApiResponseData[this.alertsApiResponseData.length - 1] + ?.name + ); + } + + async checkAlertIsActiveOrDeactive(isActive = true) { + await expect(this.page.locator("h6").nth(1)).toHaveText( + isActive ? "Alert is active" : "Alert is inactive" + ); + } + + async activeDeactiveAlert(activate = true) { + await this.page.getByRole("button", { name: "Options" }).click(); + await this.page + .getByRole("menuitem", { + name: activate ? "Activate Alert" : "Deactivate Alert", + }) + .click(); + } + + async openAnomalies() { + await this.page + .locator("span") + .filter({ hasText: "Anomalies" }) + .first() + .click(); + } + + async checkAnomaliesCount() { + const node = this.page + .locator("span") + .filter({ hasText: "Anomalies" }) + .first() + .isDisabled(); + return node; + } + async openFirstAlert() { + await this.page + .locator("a") + .filter({ + hasText: + this.alertsApiResponseData[ + this.alertsApiResponseData.length - 1 + ]?.name, + }) + .click(); + } + + async openAnomalieFromTable() { + await this.page + .locator("a") + .filter({ + hasText: + this.anomaliesApiResponseData[ + this.anomaliesApiResponseData.length - 1 + ]?.id, + }) + .click(); + } + + async openInvestigateAnomalyPage() { + await this.page + .getByRole("button", { name: "Investigate Anomaly" }) + .click(); + } + + async assertPageComponents() { + await expect(this.page.locator("h4").first()).toHaveText( + /^Anomaly #\d+(\s\w+)?$/ + ); + expect( + this.page.locator("li").filter({ hasText: "Anomalies" }) + ).toBeDefined(); + expect( + this.page.locator("li").filter({ + hasText: + this.alertsApiResponseData[ + this.alertsApiResponseData.length - 1 + ]?.name, + }) + ).toBeDefined(); + expect( + this.page.locator("li").filter({ + hasText: + this.anomaliesApiResponseData[ + this.anomaliesApiResponseData.length - 1 + ]?.id, + }) + ).toBeDefined(); + await expect(this.page.locator("h4").nth(1)).toHaveText( + "Confirm anomaly" + ); + expect( + this.page.locator("p").filter({ hasText: "Anomaly start" }) + ).toBeDefined(); + expect( + this.page.locator("p").filter({ hasText: "Anomaly end" }) + ).toBeDefined(); + expect( + this.page.locator("p").filter({ hasText: "Anomaly duration" }) + ).toBeDefined(); + expect( + this.page.locator("p").filter({ hasText: "Seasonality" }) + ).toBeDefined(); + expect( + this.page + .locator("p") + .filter({ hasText: "Deviation (Current / Predicted)" }) + ).toBeDefined(); + } + + async assertInvestigatePageComponents() { + await expect(this.page.locator("h4").first()).toHaveText( + "Investigation (not saved)" + ); + + expect( + this.page.locator("li").filter({ + hasText: + this.anomaliesApiResponseData[ + this.anomaliesApiResponseData.length - 1 + ]?.id, + }) + ).toBeDefined(); + expect( + this.page.locator("p").filter({ hasText: "Anomaly start" }) + ).toBeDefined(); + expect( + this.page.locator("p").filter({ hasText: "Anomaly end" }) + ).toBeDefined(); + expect( + this.page.locator("p").filter({ hasText: "Anomaly duration" }) + ).toBeDefined(); + expect( + this.page.locator("p").filter({ hasText: "Seasonality" }) + ).toBeDefined(); + expect( + this.page + .locator("p") + .filter({ hasText: "Deviation (Current / Predicted)" }) + ).toBeDefined(); + await expect(this.page.locator("h4").nth(1)).toHaveText( + "What went wrong and where?" + ); + await expect(this.page.locator("h4").nth(2)).toHaveText( + "Investigation preview" + ); + expect( + this.page + .locator("span") + .filter({ hasText: "What went wrong and where?" }) + ).toBeDefined(); + expect( + this.page + .locator("span") + .filter({ hasText: "An event could have caused it?" }) + ).toBeDefined(); + expect( + this.page + .locator("span") + .filter({ hasText: "Review investigation & share" }) + ).toBeDefined(); + expect( + this.page.locator("h5").filter({ hasText: "Top Contributors" }) + ).toBeDefined(); + expect( + this.page + .locator("h5") + .filter({ hasText: "Heatmap & Dimension Drills" }) + ).toBeDefined(); + expect( + this.page + .locator("h5") + .filter({ hasText: "Heatmap & Dimension Drills" }) + ).toBeDefined(); + } +} diff --git a/thirdeye-ui/e2e/pages/base.ts b/thirdeye-ui/e2e/pages/base.ts index 6cdffa050a..ab64990413 100644 --- a/thirdeye-ui/e2e/pages/base.ts +++ b/thirdeye-ui/e2e/pages/base.ts @@ -22,7 +22,7 @@ export class BasePage { } async gotoHomePage() { - await this.page.goto("http://localhost:7004"); + await this.page.goto("http://localhost:7004/#access_token=''"); await this.page.waitForURL("http://localhost:7004/home"); } } diff --git a/thirdeye-ui/e2e/pages/create-alert.ts b/thirdeye-ui/e2e/pages/create-alert.ts new file mode 100644 index 0000000000..d47508a94d --- /dev/null +++ b/thirdeye-ui/e2e/pages/create-alert.ts @@ -0,0 +1,333 @@ +/* + * Copyright 2024 StarTree Inc + * + * Licensed under the StarTree Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.startree.ai/legal/startree-community-license + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OF ANY KIND, + * either express or implied. + * + * See the License for the specific language governing permissions and limitations under + * the License. + */ +import { expect, Page } from "@playwright/test"; +import { sortBy } from "lodash"; +import { BasePage } from "./base"; + +const generateOptions = (): any[] => { + return [ + { + title: "Threshold", + description: "Threshold algorithm description", + alertTemplate: "startree-threshold", + alertTemplateForMultidimension: "startree-threshold-dx", + alertTemplateForMultidimensionQuery: "startree-threshold-query-dx", + alertTemplateForPercentile: "startree-threshold-percentile", + exampleImage: "ThresholdScreenshot", + }, + { + title: "Mean Variance Rule", + description: "Mean variance rule algorithm description", + alertTemplate: "startree-mean-variance", + alertTemplateForMultidimension: "startree-mean-variance-dx", + alertTemplateForMultidimensionQuery: + "startree-mean-variance-query-dx", + alertTemplateForPercentile: "startree-mean-variance-percentile", + exampleImage: "MeanVarianceScreenshot", + }, + { + title: "Percentage Rule", + description: "Percentage rule algorithm description", + alertTemplate: "startree-percentage-rule", + alertTemplateForMultidimension: "startree-percentage-rule-dx", + alertTemplateForMultidimensionQuery: + "startree-percentage-rule-query-dx", + alertTemplateForPercentile: "startree-percentage-percentile", + exampleImage: "PercentageRuleScreenshot", + }, + { + title: "Absolute Change Rule", + description: "Absolute change rule algorithm description", + alertTemplate: "startree-absolute-rule", + alertTemplateForMultidimension: "startree-absolute-rule-dx", + alertTemplateForMultidimensionQuery: + "startree-absolute-rule-query-dx", + alertTemplateForPercentile: "startree-absolute-percentile", + exampleImage: "AbsoluteScreenshot", + }, + { + title: "Startree-ETS", + description: "Startree ETS algorithm description", + alertTemplate: "startree-ets", + alertTemplateForMultidimension: "startree-ets-dx", + alertTemplateForMultidimensionQuery: "startree-ets-query-dx", + alertTemplateForPercentile: "startree-ets-percentile", + exampleImage: "ETSScreenshot", + }, + { + title: "Matrix Profile", + description: + "The matrix profile method is a direct anomaly detection method", + alertTemplate: "startree-matrix-profile", + alertTemplateForMultidimension: "startree-matrix-profile-dx", + alertTemplateForMultidimensionQuery: + "startree-matrix-profile-query-dx", + alertTemplateForPercentile: "startree-matrix-profile-percentile", + exampleImage: "MatrixProfileScreenshot", + }, + ]; +}; + +export class CreateAlertPage extends BasePage { + readonly page: Page; + datasetsResponseData: any; + metricsResponseData: any; + dataSourcesResponseData: any; + evaluateResponseData: any; + recommendResponseData: any; + + constructor(page: Page) { + super(page); + this.page = page; + } + + async resolveApis() { + const [datasetsApiResponse, metricsApiResponse, dataSourcesResponse] = + await Promise.all([ + this.page.waitForResponse( + (response) => + response.url().includes("/api/datasets") && + response.status() === 200 + ), + this.page.waitForResponse( + (response) => + response.url().includes("/api/metrics") && + response.status() === 200 + ), + this.page.waitForResponse( + (response) => + response.url().includes("/api/data-sources") && + response.status() === 200 + ), + ]); + const datasetInfo = await datasetsApiResponse.json(); + this.datasetsResponseData = sortBy(datasetInfo, [ + (d) => d.name.toLowerCase(), + ]); + this.metricsResponseData = await metricsApiResponse.json(); + this.dataSourcesResponseData = await dataSourcesResponse.json(); + } + + async resolveRecommendApis() { + const [recommendApiResponse] = await Promise.all([ + this.page.waitForResponse( + (response) => + response.url().includes("/api/alerts/recommend") && + response.status() === 200 + ), + ]); + + this.recommendResponseData = await recommendApiResponse.json(); + } + + async resolveEvaluateApis() { + const [evaluateApiResponse] = await Promise.all([ + this.page.waitForResponse( + (response) => + response.url().includes("/api/alerts/evaluate") && + response.status() === 200 + ), + ]); + + this.evaluateResponseData = await evaluateApiResponse.json(); + } + + async resolveMetricsCohortsApis() { + const [cohortsApiResponse] = await Promise.all([ + this.page.waitForResponse( + (response) => + response.url().includes("/api/rca/metrics/cohorts") && + response.status() === 200 + ), + ]); + const data = await cohortsApiResponse.json(); + return data; + } + + async selectDatasetAndMetric(customMetric = false) { + await this.page.getByText("Dataset Select a dataset to").click(); + await this.page + .getByRole("option", { name: this.datasetsResponseData[0].name }) + .click(); + await this.page + .getByTestId("metric-select") + .locator("div") + .nth(1) + .click(); + if (customMetric) { + await this.page + .getByRole("option", { + name: "Custom (Metric + Aggregation function)", + }) + .click(); + + return; + } + const metrics = this.metricsResponseData?.find( + (m) => m?.dataset?.name === this.datasetsResponseData[0].name + ); + await this.page.getByRole("option", { name: metrics.name }).click(); + } + + async selectStaticFields(isMultiDimensional = false, isSQLQuery = false) { + await this.page.locator("div").filter({ hasText: /^SUM$/ }).click(); + await this.page.getByLabel("SUM").check(); + await this.page.getByPlaceholder("Select granularity").click(); + await this.page.getByRole("option", { name: "Daily" }).click(); + if (!isMultiDimensional) { + await this.page + .locator("div") + .filter({ hasText: /^Single metric$/ }) + .click(); + return; + } + await this.page.getByLabel("Multiple dimensions").check(); + if (isSQLQuery) { + await this.page.getByLabel("SQL Query").check(); + return; + } + await this.page.getByLabel("Dimension recommender").check(); + } + + async selectDetectionAlgorithm(isMultiDimensional = false) { + await this.page.getByPlaceholder("Select an algorithm").click(); + const availableOptions = generateOptions(); + const algorithmOption = availableOptions.find( + (option) => + option.alertTemplate === + this.recommendResponseData?.recommendations[0]?.alert + .template?.name || + (isMultiDimensional && + option.alertTemplateForMultidimension === + this.recommendResponseData?.recommendations[0]?.alert + .template?.name) + ); + await this.page + .getByRole("heading", { + name: algorithmOption + ? `${algorithmOption.title} option 1` + : availableOptions[0].title, + }) + .click(); + } + + async goToCreateAlertPage() { + await this.page.goto("http://localhost:7004/#access_token=''"); + await this.page.waitForSelector("h4:has-text('StarTree ThirdEye')", { + timeout: 10000, + state: "visible", + }); + await this.page.goto( + "http://localhost:7004/alerts/create/new/new-user/easy-alert/" + ); + } + + async checkHeader() { + await expect(this.page.locator("h5")).toHaveText("Alert wizard"); + } + + async clickLoadChartButton() { + await this.page.getByRole("button", { name: "Load chart" }).click(); + } + + async createAlert() { + const switchElement = this.page.locator(".MuiSwitch-input"); + await switchElement.click(); + await this.page.getByRole("button", { name: "Create alert" }).click(); + await this.page + .getByTestId("alert-name-input") + .getByRole("textbox") + .click(); + await this.page + .getByTestId("alert-name-input") + .getByRole("textbox") + .fill(`Impressions_SUM_star-tree-ets_dx-${Date.now()}`); + await this.page.getByRole("button", { name: "Create alert" }).click(); + } + + async addDimensions() { + await this.page.getByRole("button", { name: "Add dimensions" }).click(); + await this.page.getByPlaceholder("Select dimensions").click(); + await this.page + .getByRole("option", { + name: this.datasetsResponseData[0]?.dimensions[0], + }) + .click(); + await this.page.waitForTimeout(1000); + const button = this.page.locator("button", { + hasText: "Generate dimensions to monitor", + }); + await button.click(); + const data = await this.resolveMetricsCohortsApis(); + await this.page + .getByRole("row", { + name: `${this.datasetsResponseData[0]?.dimensions[0]}='${ + data?.results[0].dimensionFilters[ + this.datasetsResponseData[0]?.dimensions[0] + ] + }'`, + }) + .getByRole("checkbox") + .check(); + await this.page + .getByRole("button", { name: "Add selected dimensions" }) + .click(); + } + + async addSQLQuery() { + const textarea = await this.page.locator("textarea:first-of-type"); + const placeholderText = await textarea.getAttribute("placeholder"); + if (placeholderText) { + textarea.fill(placeholderText); + } + await this.page + .getByRole("button", { name: "Run enumerations" }) + .click(); + } + + async addAdvancedOptions() { + await this.page + .getByRole("button", { name: "Add advanced options" }) + .click(); + await this.page.getByTestId("optionedselect-daysOfWeek").click(); + await this.page + .getByRole("option", { + name: "MONDAY", + }) + .click(); + await this.page.getByTestId("optionedselect-daysOfWeek").click(); + await this.page + .getByRole("option", { + name: "TUESDAY", + }) + .click(); + await this.page.getByRole("button", { name: "Apply filter" }).click(); + await this.page.getByRole("button", { name: "Reload preview" }).click(); + } + + async addCustomMetric() { + const metrics = this.metricsResponseData?.find( + (m) => m?.dataset?.name === this.datasetsResponseData[0].name + ); + const textarea = await this.page.locator("textarea:first-of-type"); + textarea.fill(`SUM(${metrics.name})`); + await this.page.getByPlaceholder("Select granularity").click(); + await this.page.getByRole("option", { name: "Daily" }).click(); + await this.page + .locator("div") + .filter({ hasText: /^Single metric$/ }) + .click(); + } +} diff --git a/thirdeye-ui/e2e/pages/home.ts b/thirdeye-ui/e2e/pages/home.ts index 78ade57718..993d386092 100644 --- a/thirdeye-ui/e2e/pages/home.ts +++ b/thirdeye-ui/e2e/pages/home.ts @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and limitations under * the License. */ -import { Page, expect } from "@playwright/test"; +import { expect, Page } from "@playwright/test"; import { BasePage } from "./base"; // import { getUiAnomaly } from '../../src/app/utils/anomalies/anomalies.util' diff --git a/thirdeye-ui/e2e/pages/onboarding.ts b/thirdeye-ui/e2e/pages/onboarding.ts new file mode 100644 index 0000000000..0111351038 --- /dev/null +++ b/thirdeye-ui/e2e/pages/onboarding.ts @@ -0,0 +1,101 @@ +/* + * Copyright 2024 StarTree Inc + * + * Licensed under the StarTree Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.startree.ai/legal/startree-community-license + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OF ANY KIND, + * either express or implied. + * + * See the License for the specific language governing permissions and limitations under + * the License. + */ +import { expect, Page } from "@playwright/test"; +import { BasePage } from "./base"; + +export class OnboardingPage extends BasePage { + readonly page: Page; + + constructor(page: Page) { + super(page); + this.page = page; + } + + async resolveApis() { + const [count, workspaces, datasets] = await Promise.all([ + this.page.waitForResponse( + (response) => + response.url().includes("/api/alerts/count") && + response.status() === 200 + ), + this.page.waitForResponse( + (response) => + response.url().includes("/api/workspaces") && + response.status() === 200 + ), + this.page.waitForResponse( + (response) => + response.url().includes("/api/datasets") && + response.status() === 200 + ), + ]); + } + + async resolveCreateAlertApis() { + const [datasetsApiResponse, metricsApiResponse, dataSourcesResponse] = + await Promise.all([ + this.page.waitForResponse( + (response) => + response.url().includes("/api/datasets") && + response.status() === 200 + ), + this.page.waitForResponse( + (response) => + response.url().includes("/api/metrics") && + response.status() === 200 + ), + this.page.waitForResponse( + (response) => + response.url().includes("/api/data-sources") && + response.status() === 200 + ), + ]); + } + + async goToWelcomeLanding() { + await this.page.goto("http://localhost:7004/#access_token=''"); + await this.page.waitForSelector("h4:has-text('StarTree ThirdEye')", { + timeout: 10000, + state: "visible", + }); + await this.page.goto("http://localhost:7004/welcome/landing"); + } + + async checkHeader() { + await expect(this.page.locator("h4")).toHaveText( + "Let's create your first setup" + ); + await expect(this.page.locator("h5")).toHaveText( + "Complete the following steps." + ); + } + + async checkCreateAlertHeader() { + await expect(this.page.locator("h4")).toHaveText("Create Alert"); + } + + async checkCartHeader() { + await expect(this.page.locator("h6").first()).toHaveText( + "Review and configure data" + ); + await expect(this.page.locator("h6").nth(1)).toHaveText( + "Create my first alert" + ); + } + + async clickCreateAlertButton() { + await this.page.getByRole("button", { name: "Create Alert" }).click(); + } +} diff --git a/thirdeye-ui/e2e/tests/alert-details.spec.ts b/thirdeye-ui/e2e/tests/alert-details.spec.ts new file mode 100644 index 0000000000..ae2e79e3fa --- /dev/null +++ b/thirdeye-ui/e2e/tests/alert-details.spec.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2024 StarTree Inc + * + * Licensed under the StarTree Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.startree.ai/legal/startree-community-license + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OF ANY KIND, + * either express or implied. + * + * See the License for the specific language governing permissions and limitations under + * the License. + */ +import { test } from "@playwright/test"; +import { AlertDetailsPage } from "../pages/alert-detail"; + +test("View anomalies for alerts", async ({ page }) => { + const alertDetailsPage = new AlertDetailsPage(page); + await alertDetailsPage.goToAlertDetailsPage(); + await alertDetailsPage.resolveApis(); + await alertDetailsPage.checkHeader(); + await alertDetailsPage.openFirstAlert(); + await alertDetailsPage.checkAlertHeader(); + const anomalyDisabled = await alertDetailsPage.checkAnomaliesCount(); + if (anomalyDisabled) { + return; + } + await alertDetailsPage.openAnomalies(); + await alertDetailsPage.resolveAnomaliesApis(); + await alertDetailsPage.openAnomalieFromTable(); + await alertDetailsPage.assertPageComponents(); +}); + +test("Active and Deactivate alerts", async ({ page }) => { + const alertDetailsPage = new AlertDetailsPage(page); + await alertDetailsPage.goToAlertDetailsPage(); + await alertDetailsPage.resolveApis(); + await alertDetailsPage.checkHeader(); + await alertDetailsPage.openFirstAlert(); + await alertDetailsPage.checkAlertHeader(); + const anomalyDisabled = await alertDetailsPage.checkAnomaliesCount(); + if (anomalyDisabled) { + return; + } + await alertDetailsPage.checkAlertIsActiveOrDeactive(true); + await alertDetailsPage.activeDeactiveAlert(false); + await alertDetailsPage.resolveApis(); + await alertDetailsPage.checkAlertIsActiveOrDeactive(false); + await alertDetailsPage.activeDeactiveAlert(true); + await alertDetailsPage.resolveApis(); + await alertDetailsPage.checkAlertIsActiveOrDeactive(true); +}); diff --git a/thirdeye-ui/e2e/tests/anomalies.spec.ts b/thirdeye-ui/e2e/tests/anomalies.spec.ts new file mode 100644 index 0000000000..fb986424ae --- /dev/null +++ b/thirdeye-ui/e2e/tests/anomalies.spec.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2024 StarTree Inc + * + * Licensed under the StarTree Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.startree.ai/legal/startree-community-license + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OF ANY KIND, + * either express or implied. + * + * See the License for the specific language governing permissions and limitations under + * the License. + */ +import { test } from "@playwright/test"; +import { AlertDetailsPage } from "../pages/alert-detail"; + +test("Investigate Anomaly", async ({ page }) => { + const alertDetailsPage = new AlertDetailsPage(page); + await alertDetailsPage.goToAlertDetailsPage(); + await alertDetailsPage.resolveApis(); + await alertDetailsPage.checkHeader(); + await alertDetailsPage.openFirstAlert(); + await alertDetailsPage.checkAlertHeader(); + const anomalyDisabled = await alertDetailsPage.checkAnomaliesCount(); + if (anomalyDisabled) { + return; + } + await alertDetailsPage.openAnomalies(); + await alertDetailsPage.resolveAnomaliesApis(); + await alertDetailsPage.openAnomalieFromTable(); + await alertDetailsPage.openInvestigateAnomalyPage(); + await alertDetailsPage.resolveInvestigateAnomalyPageApis(); + await alertDetailsPage.assertInvestigatePageComponents(); +}); diff --git a/thirdeye-ui/e2e/tests/create-alert.spec.ts b/thirdeye-ui/e2e/tests/create-alert.spec.ts new file mode 100644 index 0000000000..7962139f84 --- /dev/null +++ b/thirdeye-ui/e2e/tests/create-alert.spec.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2024 StarTree Inc + * + * Licensed under the StarTree Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.startree.ai/legal/startree-community-license + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OF ANY KIND, + * either express or implied. + * + * See the License for the specific language governing permissions and limitations under + * the License. + */ +import { test } from "@playwright/test"; +import { CreateAlertPage } from "../pages/create-alert"; + +test("Create Simple Alert", async ({ page }) => { + const createAlertPage = new CreateAlertPage(page); + await createAlertPage.goToCreateAlertPage(); + await createAlertPage.resolveApis(); + await createAlertPage.checkHeader(); + await createAlertPage.selectDatasetAndMetric(); + await createAlertPage.selectStaticFields(); + await createAlertPage.resolveRecommendApis(); + await createAlertPage.selectDetectionAlgorithm(); + await createAlertPage.resolveEvaluateApis(); + await createAlertPage.createAlert(); +}); + +test("Create Multi Dimensions Alert", async ({ page }) => { + const createAlertPage = new CreateAlertPage(page); + await createAlertPage.goToCreateAlertPage(); + await createAlertPage.resolveApis(); + await createAlertPage.checkHeader(); + await createAlertPage.selectDatasetAndMetric(); + await createAlertPage.selectStaticFields(true); + await createAlertPage.addDimensions(); + await createAlertPage.resolveRecommendApis(); + await createAlertPage.selectDetectionAlgorithm(true); + await createAlertPage.resolveEvaluateApis(); + await createAlertPage.createAlert(); +}); + +test("Create Multi Dimensions SQL Alert", async ({ page }) => { + const createAlertPage = new CreateAlertPage(page); + await createAlertPage.goToCreateAlertPage(); + await createAlertPage.resolveApis(); + await createAlertPage.checkHeader(); + await createAlertPage.selectDatasetAndMetric(); + await createAlertPage.selectStaticFields(true, true); + await createAlertPage.addSQLQuery(); + await createAlertPage.resolveRecommendApis(); + await createAlertPage.selectDetectionAlgorithm(true); + await createAlertPage.clickLoadChartButton(); + await createAlertPage.resolveEvaluateApis(); + await createAlertPage.createAlert(); +}); + +test("Create Alert With Advanced Options", async ({ page }) => { + const createAlertPage = new CreateAlertPage(page); + await createAlertPage.goToCreateAlertPage(); + await createAlertPage.resolveApis(); + await createAlertPage.checkHeader(); + await createAlertPage.selectDatasetAndMetric(); + await createAlertPage.selectStaticFields(); + await createAlertPage.resolveRecommendApis(); + await createAlertPage.selectDetectionAlgorithm(); + await createAlertPage.resolveEvaluateApis(); + await createAlertPage.addAdvancedOptions(); + await createAlertPage.resolveEvaluateApis(); + await createAlertPage.createAlert(); +}); + +test("Create Alert With Custom Metric", async ({ page }) => { + const createAlertPage = new CreateAlertPage(page); + await createAlertPage.goToCreateAlertPage(); + await createAlertPage.resolveApis(); + await createAlertPage.checkHeader(); + await createAlertPage.selectDatasetAndMetric(true); + await createAlertPage.addCustomMetric(); + await createAlertPage.resolveRecommendApis(); + await createAlertPage.selectDetectionAlgorithm(); + await createAlertPage.resolveEvaluateApis(); + await createAlertPage.createAlert(); +}); diff --git a/thirdeye-ui/e2e/tests/onboarding.spec.ts b/thirdeye-ui/e2e/tests/onboarding.spec.ts new file mode 100644 index 0000000000..5b74a5303c --- /dev/null +++ b/thirdeye-ui/e2e/tests/onboarding.spec.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2024 StarTree Inc + * + * Licensed under the StarTree Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at http://www.startree.ai/legal/startree-community-license + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OF ANY KIND, + * either express or implied. + * + * See the License for the specific language governing permissions and limitations under + * the License. + */ +import { test } from "@playwright/test"; +import { OnboardingPage } from "../pages/onboarding"; + +test("Onboarding page", async ({ page }) => { + const onboardingPage = new OnboardingPage(page); + await page.route("*/**/api/alerts/count", async (route) => { + const json = { + count: 0, + }; + await route.fulfill({ json }); + }); + await onboardingPage.goToWelcomeLanding(); + await onboardingPage.resolveApis(); + await onboardingPage.checkHeader(); + await onboardingPage.checkCartHeader(); + await onboardingPage.clickCreateAlertButton(); + await onboardingPage.resolveCreateAlertApis(); + await onboardingPage.checkCreateAlertHeader(); +}); + +test("Add Datasets Button on Onboarding Page", async ({ page }) => { + const onboardingPage = new OnboardingPage(page); + await page.route("*/**/api/alerts/count", async (route) => { + const json = { + count: 0, + }; + await route.fulfill({ json }); + }); + await page.route("*/**/api/datasets", async (route) => { + const json = {}; + await route.fulfill({ json }); + }); + await onboardingPage.goToWelcomeLanding(); + await onboardingPage.resolveApis(); + await onboardingPage.checkHeader(); + await onboardingPage.checkConfigureCardHeader(); + await onboardingPage.clickConfigureDataButton(); + await onboardingPage.resolveDataSourcesApi(); + await onboardingPage.checkConfigurePageHeader(); +}); diff --git a/thirdeye-ui/src/app/components/additional-filters-drawer/additional-filters-drawer.component.tsx b/thirdeye-ui/src/app/components/additional-filters-drawer/additional-filters-drawer.component.tsx index af94d2735c..0487004133 100644 --- a/thirdeye-ui/src/app/components/additional-filters-drawer/additional-filters-drawer.component.tsx +++ b/thirdeye-ui/src/app/components/additional-filters-drawer/additional-filters-drawer.component.tsx @@ -58,7 +58,8 @@ export const AdditonalFiltersDrawer: FunctionComponent { + const onSubmit = (e: React.FormEvent): void => { + e.preventDefault(); onApply(localCopyOfProperties); }; diff --git a/thirdeye-ui/src/app/pages/alerts-create-page/alerts-create-easy-page/alerts-create-easy-page.component.tsx b/thirdeye-ui/src/app/pages/alerts-create-page/alerts-create-easy-page/alerts-create-easy-page.component.tsx index 0247270b23..071bfe9972 100644 --- a/thirdeye-ui/src/app/pages/alerts-create-page/alerts-create-easy-page/alerts-create-easy-page.component.tsx +++ b/thirdeye-ui/src/app/pages/alerts-create-page/alerts-create-easy-page/alerts-create-easy-page.component.tsx @@ -275,8 +275,8 @@ export const AlertsCreateEasyPage: FunctionComponent = () => { () => !selectedTable || !selectedMetric || - !aggregationFunction || - !granularity || + (selectedMetric !== t("label.custom-metric-aggregation") && + (!aggregationFunction || !granularity)) || !anomalyDetection || !algorithmOption, [ @@ -288,6 +288,7 @@ export const AlertsCreateEasyPage: FunctionComponent = () => { algorithmOption, ] ); + const alertTemplateForEvaluate = useMemo(() => { let alertTemplateToFind = isMultiDimensionAlert ? algorithmOption?.algorithmOption.alertTemplateForMultidimension @@ -808,11 +809,11 @@ export const AlertsCreateEasyPage: FunctionComponent = () => { algorithm: AvailableAlgorithmOption ): void => { if ( - !algorithm || !selectedTable || + !granularity || !selectedMetric || - !aggregationFunction || - !granularity + (selectedMetric !== t("label.custom-metric-aggregation") && + (!aggregationFunction || !granularity)) ) { return; } @@ -1907,7 +1908,10 @@ export const AlertsCreateEasyPage: FunctionComponent = () => { } variant="outlined" - onClick={() => { + onClick={( + e + ) => { + e.preventDefault(); setShowAdvancedOptions( true );