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
);