From 9605871c47bea903b797e72c8eac8617d9e78647 Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Mon, 17 Nov 2025 17:15:58 +0200 Subject: [PATCH 1/8] fix: refresh button in sessions hides when we resize the sessions table frame to the smallest --- .../table/filters/filterBySessionState.tsx | 30 +++++++++++++++---- .../deployments/sessions/table/table.tsx | 2 ++ src/constants/resize.constants.ts | 2 +- .../components/session.interface.ts | 1 + 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/components/organisms/deployments/sessions/table/filters/filterBySessionState.tsx b/src/components/organisms/deployments/sessions/table/filters/filterBySessionState.tsx index a92ac359f9..123824ae72 100644 --- a/src/components/organisms/deployments/sessions/table/filters/filterBySessionState.tsx +++ b/src/components/organisms/deployments/sessions/table/filters/filterBySessionState.tsx @@ -11,7 +11,12 @@ import { DropdownButton } from "@components/molecules"; import { FilterIcon } from "@assets/image/icons"; -export const SessionsTableFilter = ({ onChange, filtersData, selectedState }: SessionTableFilterProps) => { +export const SessionsTableFilter = ({ + onChange, + filtersData, + selectedState, + isCompactMode = false, +}: SessionTableFilterProps) => { const { t } = useTranslation("deployments", { keyPrefix: "sessions.table.statuses" }); const validatedState = @@ -27,7 +32,11 @@ export const SessionsTableFilter = ({ onChange, filtersData, selectedState }: Se ); const filterClass = (state?: SessionStateType) => - cn("h-8 whitespace-nowrap border-0 pr-4 text-white hover:bg-transparent", state && getSessionStateColor(state)); + cn( + "h-8 border-0 text-white hover:bg-transparent", + isCompactMode ? "pr-2" : "whitespace-nowrap pr-4", + state && getSessionStateColor(state) + ); return ( ); diff --git a/src/components/organisms/deployments/sessions/table/table.tsx b/src/components/organisms/deployments/sessions/table/table.tsx index ddc0f53054..d537abbc0f 100644 --- a/src/components/organisms/deployments/sessions/table/table.tsx +++ b/src/components/organisms/deployments/sessions/table/table.tsx @@ -70,6 +70,7 @@ export const SessionsTable = () => { }); const prevDeploymentsRef = useRef(deployments); + const isCompactMode = leftSideWidth < 20; const processStateFilter = (stateFilter?: string | null) => { if (!stateFilter) return ""; @@ -390,6 +391,7 @@ export const SessionsTable = () => {
navigateInSessions(sessionIdFromParams || "", sessionState)} selectedState={urlSessionStateFilter} /> diff --git a/src/constants/resize.constants.ts b/src/constants/resize.constants.ts index fa68910d8c..7ef92e386e 100644 --- a/src/constants/resize.constants.ts +++ b/src/constants/resize.constants.ts @@ -9,7 +9,7 @@ export const defaultSystemLogSize = { export const defaultSplitFrameSize = { max: 70, min: 15, - initial: 20, + initial: 30, }; /** diff --git a/src/interfaces/components/session.interface.ts b/src/interfaces/components/session.interface.ts index 9fc6ea664c..4cfa8cbbc6 100644 --- a/src/interfaces/components/session.interface.ts +++ b/src/interfaces/components/session.interface.ts @@ -9,6 +9,7 @@ export interface SessionTableFilterProps { filtersData: SessionStatsFilterType; defaultValue?: SessionStateKeyType; selectedState?: SessionStateType; + isCompactMode?: boolean; } export interface SessionsTableRowProps { From cbad6d7c77ac4526ada55de7e61025639ed18351 Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Wed, 19 Nov 2025 12:35:10 +0200 Subject: [PATCH 2/8] fix: sessions table refactor --- e2e/pages/index.ts | 1 + e2e/pages/webhookSession.ts | 105 +++++++ e2e/project/sessionsCompactMode.spec.ts | 263 ++++++++++++++++++ e2e/project/splitScreen.spec.ts | 140 ++++++++++ e2e/project/webhookSessionTriggered.spec.ts | 102 +------ playwright.config.ts | 12 +- src/assets/image/icons/Clock.svg | 2 +- src/assets/image/icons/Info.svg | 2 +- src/assets/image/icons/InfoNoCircle.svg | 1 + src/assets/image/icons/Link.svg | 2 + src/assets/image/icons/Webhook.svg | 2 +- src/assets/image/icons/index.ts | 4 + .../image/icons/sidebar/Connections.svg | 2 +- src/autokitteh | 2 +- src/components/atoms/icons/iconSvg.tsx | 2 +- .../molecules/popover/popoverTrigger.tsx | 4 +- .../connections/integrations/google/add.tsx | 4 +- .../connections/integrations/google/edit.tsx | 2 +- src/components/organisms/deployments/index.ts | 1 + .../organisms/deployments/sessions/index.ts | 1 + .../sessions/table/sessionInfoPopover.tsx | 240 ++++++++++++++++ .../deployments/sessions/table/table.tsx | 37 ++- .../deployments/sessions/table/tableList.tsx | 6 +- .../deployments/sessions/table/tableRow.tsx | 117 ++++++-- .../organisms/files/projectFiles.tsx | 4 +- src/components/organisms/splitFrame.tsx | 9 +- .../formsPerIntegrationsMapping.constants.ts | 2 +- .../integrationVariablesMapping.constants.ts | 2 +- src/constants/index.ts | 1 + .../lists/connections/options.constants.ts | 2 +- src/constants/resize.constants.ts | 6 + src/hooks/useConnectionForm.ts | 9 +- .../components/popover.interface.ts | 2 + .../components/session.interface.ts | 4 + src/interfaces/models/session.interface.ts | 1 + .../sharedBetweenProjectsStore.interface.ts | 6 +- src/locales/en/deployments/translation.json | 16 +- src/models/session.model.ts | 2 +- src/store/useSharedBetweenProjectsStore.ts | 53 +++- src/utilities/getSessionTriggerType.utils.ts | 23 ++ src/utilities/index.ts | 2 + src/validations/connection.schema.ts | 6 +- vite.config.ts | 6 +- 43 files changed, 1027 insertions(+), 183 deletions(-) create mode 100644 e2e/pages/webhookSession.ts create mode 100644 e2e/project/sessionsCompactMode.spec.ts create mode 100644 e2e/project/splitScreen.spec.ts create mode 100644 src/assets/image/icons/InfoNoCircle.svg create mode 100644 src/assets/image/icons/Link.svg create mode 100644 src/components/organisms/deployments/sessions/table/sessionInfoPopover.tsx create mode 100644 src/utilities/getSessionTriggerType.utils.ts diff --git a/e2e/pages/index.ts b/e2e/pages/index.ts index 2f6f06788d..2d6e5e5bca 100644 --- a/e2e/pages/index.ts +++ b/e2e/pages/index.ts @@ -1,2 +1,3 @@ export { DashboardPage } from "./dashboard"; export { ProjectPage } from "./project"; +export { WebhookSessionPage } from "./webhookSession"; diff --git a/e2e/pages/webhookSession.ts b/e2e/pages/webhookSession.ts new file mode 100644 index 0000000000..33d71b275e --- /dev/null +++ b/e2e/pages/webhookSession.ts @@ -0,0 +1,105 @@ +import { expect, type APIRequestContext, type Page } from "@playwright/test"; +import randomatic from "randomatic"; + +import { DashboardPage } from "./dashboard"; +import { waitForToast } from "e2e/utils"; +import { waitForLoadingOverlayGone } from "e2e/utils/waitForLoadingOverlayToDisappear"; + +export class WebhookSessionPage { + private readonly page: Page; + private readonly request: APIRequestContext; + private readonly dashboardPage: DashboardPage; + public projectName: string; + + constructor(page: Page, request: APIRequestContext) { + this.page = page; + this.request = request; + this.dashboardPage = new DashboardPage(page); + this.projectName = `test_${randomatic("Aa", 4)}`; + } + + async waitForFirstCompletedSession(timeoutMs = 120000) { + await expect(async () => { + const refreshButton = this.page.locator('button[aria-label="Refresh"]'); + const isDisabled = await refreshButton.evaluate((element) => (element as HTMLButtonElement).disabled); + + if (!isDisabled) { + await refreshButton.click(); + } + + const completedSession = await this.page.getByRole("button", { name: "1 Completed" }).isVisible(); + + expect(completedSession).toBe(true); + + return true; + }).toPass({ + timeout: timeoutMs, + intervals: [3000], + }); + } + + async setupProjectAndTriggerSession() { + await this.page.goto("/"); + + await this.page.getByRole("heading", { name: /^Welcome to .+$/, level: 1 }).isVisible(); + + try { + await this.page.locator('button[aria-label="Start From Template"]').click({ timeout: 3000 }); + + await expect(this.page.getByText("Start From Template")).toBeVisible(); + + await this.page.getByLabel("Categories").click(); + await this.page.getByRole("option", { name: "Samples" }).click(); + await this.page.locator("body").click({ position: { x: 0, y: 0 } }); + await this.page.locator('button[aria-label="Create Project From Template: HTTP"]').scrollIntoViewIfNeeded(); + await this.page.locator('button[aria-label="Create Project From Template: HTTP"]').click({ timeout: 2000 }); + + await this.page.getByPlaceholder("Enter project name").fill(this.projectName); + await this.page.locator('button[aria-label="Create"]').click(); + await this.page.waitForURL(/\/explorer\/settings/); + await expect(this.page.getByRole("heading", { name: "Configuration" })).toBeVisible(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + await this.dashboardPage.createProjectFromTemplate(this.projectName); + } + + await waitForLoadingOverlayGone(this.page); + await this.page.locator('button[aria-label="Open Triggers Section"]').click(); + await expect( + this.page.locator(`button[aria-label='Trigger information for "receive_http_get_or_head"']`) + ).toBeVisible(); + await this.page.locator(`button[aria-label='Trigger information for "receive_http_get_or_head"']`).hover(); + + const copyButton = await this.page.waitForSelector('[data-testid="copy-receive_http_get_or_head-webhook-url"]'); + const webhookUrl = await copyButton.getAttribute("value"); + + if (!webhookUrl) { + throw new Error("Failed to get webhook URL from button value attribute"); + } + + await this.page.locator('button[aria-label="Deploy project"]').click(); + + const toast = await waitForToast(this.page, "Project deployment completed successfully"); + await expect(toast).toBeVisible(); + + const response = await this.request.get(webhookUrl, { + timeout: 5000, + }); + + if (!response.ok()) { + throw new Error(`Webhook request failed with status ${response.status()}`); + } + + await this.page.locator('button[aria-label="Deployments"]').click(); + await expect(this.page.getByText("Deployment History")).toBeVisible(); + + await expect(this.page.getByRole("heading", { name: "Configuration" })).toBeVisible(); + await this.page.locator('button[aria-label="Close Project Settings"]').click(); + + await expect(this.page.getByText("Active").first()).toBeVisible(); + const deploymentId = this.page.getByText(/bld_*/); + await expect(deploymentId).toBeVisible(); + + await this.waitForFirstCompletedSession(); + } +} diff --git a/e2e/project/sessionsCompactMode.spec.ts b/e2e/project/sessionsCompactMode.spec.ts new file mode 100644 index 0000000000..5c726b0de2 --- /dev/null +++ b/e2e/project/sessionsCompactMode.spec.ts @@ -0,0 +1,263 @@ +import type { APIRequestContext, Page } from "@playwright/test"; + +import { expect, test } from "../fixtures"; +import { WebhookSessionPage } from "e2e/pages"; + +interface SetupParams { + page: Page; + request: APIRequestContext; +} + +test.describe("Sessions Table Compact Mode Suite", () => { + test.beforeEach(async ({ page, request }: SetupParams) => { + const webhookSessionPage = new WebhookSessionPage(page, request); + await webhookSessionPage.setupProjectAndTriggerSession(); + }); + + test("Should display trigger icons when sessions table is in compact mode", async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + const sessionsButton = page.locator('button[aria-label="Sessions"]'); + await sessionsButton.click(); + + await page.waitForTimeout(2000); + + const sessionsTableFrame = page.locator("#sessions-table"); + await expect(sessionsTableFrame).toBeVisible(); + + const resizeButton = page.locator("#sessions-table-resize-button"); + await expect(resizeButton).toBeVisible(); + + const resizeButtonBox = await resizeButton.boundingBox(); + if (!resizeButtonBox) { + throw new Error("Resize button not found"); + } + + const viewportSize = page.viewportSize(); + if (!viewportSize) { + throw new Error("Viewport size not available"); + } + + const targetX = viewportSize.width * 0.22; + + await page.mouse.move( + resizeButtonBox.x + resizeButtonBox.width / 2, + resizeButtonBox.y + resizeButtonBox.height / 2 + ); + await page.mouse.down(); + await page.mouse.move(targetX, resizeButtonBox.y + resizeButtonBox.height / 2); + await page.mouse.up(); + + await page.waitForTimeout(500); + + const startTimeCell = sessionsTableFrame.locator('td[aria-label^="Session "]').first(); + const infoButtons = startTimeCell.locator('button[aria-label^="Additional information session"]'); + const hasIcon = await infoButtons.count(); + expect(hasIcon).toBeGreaterThan(0); + }); + + test("Should display text when sessions table is not in compact mode", async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + const sessionsButton = page.locator('button[aria-label="Sessions"]'); + await sessionsButton.click(); + + await page.waitForTimeout(2000); + + const sessionsTableFrame = page.locator("#sessions-table"); + await expect(sessionsTableFrame).toBeVisible(); + + const resizeButton = page.locator("#sessions-table-resize-button"); + await expect(resizeButton).toBeVisible(); + + const resizeButtonBox = await resizeButton.boundingBox(); + if (!resizeButtonBox) { + throw new Error("Resize button not found"); + } + + const viewportSize = page.viewportSize(); + if (!viewportSize) { + throw new Error("Viewport size not available"); + } + + const targetX = viewportSize.width * 0.5; + + await page.mouse.move( + resizeButtonBox.x + resizeButtonBox.width / 2, + resizeButtonBox.y + resizeButtonBox.height / 2 + ); + await page.mouse.down(); + await page.mouse.move(targetX, resizeButtonBox.y + resizeButtonBox.height / 2); + await page.mouse.up(); + + await page.waitForTimeout(500); + + const sourceColumnCell = sessionsTableFrame.locator('td[aria-label^="Session "]').first(); + const cellText = await sourceColumnCell.textContent(); + + expect(cellText).toBeTruthy(); + expect(cellText?.trim().length).toBeGreaterThan(0); + }); + + test("Should toggle between icon and text when resizing", async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + const sessionsButton = page.locator('button[aria-label="Sessions"]'); + await sessionsButton.click(); + + await page.waitForTimeout(2000); + + const sessionsTableFrame = page.locator("#sessions-table"); + await expect(sessionsTableFrame).toBeVisible(); + + const resizeButton = page.locator("#sessions-table-resize-button"); + await expect(resizeButton).toBeVisible(); + + const resizeButtonBox = await resizeButton.boundingBox(); + if (!resizeButtonBox) { + throw new Error("Resize button not found"); + } + + const viewportSize = page.viewportSize(); + if (!viewportSize) { + throw new Error("Viewport size not available"); + } + + const wideTargetX = viewportSize.width * 0.5; + await page.mouse.move( + resizeButtonBox.x + resizeButtonBox.width / 2, + resizeButtonBox.y + resizeButtonBox.height / 2 + ); + await page.mouse.down(); + await page.mouse.move(wideTargetX, resizeButtonBox.y + resizeButtonBox.height / 2); + await page.mouse.up(); + + await page.waitForTimeout(500); + + const sourceColumnCell = sessionsTableFrame.locator('td[aria-label^="Session "]').first(); + const initialText = await sourceColumnCell.textContent(); + expect(initialText?.trim().length).toBeGreaterThan(0); + + const newResizeButtonBox = await resizeButton.boundingBox(); + if (!newResizeButtonBox) { + throw new Error("Resize button not found after first resize"); + } + + const narrowTargetX = viewportSize.width * 0.15; + await page.mouse.move( + newResizeButtonBox.x + newResizeButtonBox.width / 2, + newResizeButtonBox.y + newResizeButtonBox.height / 2 + ); + await page.mouse.down(); + await page.mouse.move(narrowTargetX, newResizeButtonBox.y + newResizeButtonBox.height / 2); + await page.mouse.up(); + + await page.waitForTimeout(500); + + const hasIcon = await sourceColumnCell.locator('button[aria-label^="Additional information session"]').count(); + expect(hasIcon).toBeGreaterThan(0); + }); + + test("Should persist compact mode setting after navigation", async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + const sessionsButton = page.locator('button[aria-label="Sessions"]'); + await sessionsButton.click(); + + await page.waitForTimeout(2000); + + const sessionsTableFrame = page.locator("#sessions-table"); + await expect(sessionsTableFrame).toBeVisible(); + + const resizeButton = page.locator("#sessions-table-resize-button"); + await expect(resizeButton).toBeVisible(); + + const resizeButtonBox = await resizeButton.boundingBox(); + if (!resizeButtonBox) { + throw new Error("Resize button not found"); + } + + const viewportSize = page.viewportSize(); + if (!viewportSize) { + throw new Error("Viewport size not available"); + } + + const targetX = viewportSize.width * 0.15; + + await page.mouse.move( + resizeButtonBox.x + resizeButtonBox.width / 2, + resizeButtonBox.y + resizeButtonBox.height / 2 + ); + await page.mouse.down(); + await page.mouse.move(targetX, resizeButtonBox.y + resizeButtonBox.height / 2); + await page.mouse.up(); + + await page.waitForTimeout(500); + + let sourceColumnCell = sessionsTableFrame.locator('td[aria-label^="Session "]').first(); + const hasIconBefore = await sourceColumnCell + .locator('button[aria-label^="Additional information session"]') + .count(); + expect(hasIconBefore).toBeGreaterThan(0); + + const deploymentsButton = page.locator('button[aria-label="Deployments"]'); + await deploymentsButton.click(); + await page.waitForTimeout(500); + + await sessionsButton.click(); + await page.waitForTimeout(500); + + sourceColumnCell = sessionsTableFrame.locator('td[aria-label^="Session "]').first(); + const hasIconAfter = await sourceColumnCell + .locator('button[aria-label^="Additional information session"]') + .count(); + expect(hasIconAfter).toBeGreaterThan(0); + }); + + test("Should display Webhook icon for webhook sessions in compact mode", async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + const sessionsButton = page.locator('button[aria-label="Sessions"]'); + await sessionsButton.click(); + + await page.waitForTimeout(2000); + + const sessionsTableFrame = page.locator("#sessions-table"); + await expect(sessionsTableFrame).toBeVisible(); + + const resizeButton = page.locator("#sessions-table-resize-button"); + await expect(resizeButton).toBeVisible(); + + const resizeButtonBox = await resizeButton.boundingBox(); + if (!resizeButtonBox) { + throw new Error("Resize button not found"); + } + + const viewportSize = page.viewportSize(); + if (!viewportSize) { + throw new Error("Viewport size not available"); + } + + const targetX = viewportSize.width * 0.15; + + await page.mouse.move( + resizeButtonBox.x + resizeButtonBox.width / 2, + resizeButtonBox.y + resizeButtonBox.height / 2 + ); + await page.mouse.down(); + await page.mouse.move(targetX, resizeButtonBox.y + resizeButtonBox.height / 2); + await page.mouse.up(); + + await page.waitForTimeout(500); + + const iconButton = sessionsTableFrame + .locator('td[aria-label^="Session "] button[aria-label^="Additional information session"]') + .first(); + const icon = iconButton.locator("svg").first(); + + await expect(iconButton).toBeVisible(); + + const iconClass = await icon.getAttribute("class"); + expect(iconClass).toContain("h-4 w-4"); + }); +}); diff --git a/e2e/project/splitScreen.spec.ts b/e2e/project/splitScreen.spec.ts new file mode 100644 index 0000000000..51b77847d8 --- /dev/null +++ b/e2e/project/splitScreen.spec.ts @@ -0,0 +1,140 @@ +import { expect, test } from "../fixtures"; + +test.describe("Split Screen Suite", () => { + test.beforeEach(async ({ dashboardPage }) => { + await dashboardPage.createProjectFromMenu(); + }); + + test("Should show project files panel by default", async ({ page }) => { + const filesHeading = page.getByRole("heading", { name: "Files", exact: true }); + await expect(filesHeading).toBeVisible(); + + const hideFilesButton = page.locator('button[id="hide-project-files-button"]'); + await expect(hideFilesButton).toBeVisible(); + }); + + test("Should hide project files panel when close button is clicked", async ({ page }) => { + const filesHeading = page.getByRole("heading", { name: "Files", exact: true }); + await expect(filesHeading).toBeVisible(); + + const hideFilesButton = page.locator('button[id="hide-project-files-button"]'); + await hideFilesButton.click(); + + await expect(filesHeading).not.toBeVisible(); + + const showFilesButton = page.locator('button[id="show-project-files-button"]'); + await expect(showFilesButton).toBeVisible(); + }); + + test("Should be able to resize split screen", async ({ page }) => { + const filesHeading = page.getByRole("heading", { name: "Files", exact: true }); + await expect(filesHeading).toBeVisible(); + + const resizeButton = page.locator('[data-direction="horizontal"]').first(); + await expect(resizeButton).toBeVisible(); + + const resizeButtonBox = await resizeButton.boundingBox(); + if (!resizeButtonBox) { + throw new Error("Resize button not found"); + } + + await page.mouse.move( + resizeButtonBox.x + resizeButtonBox.width / 2, + resizeButtonBox.y + resizeButtonBox.height / 2 + ); + await page.mouse.down(); + await page.mouse.move(resizeButtonBox.x + 100, resizeButtonBox.y + resizeButtonBox.height / 2); + await page.mouse.up(); + + await expect(filesHeading).toBeVisible(); + }); + + test("Should persist split screen width after navigation", async ({ page }) => { + const filesHeading = page.getByRole("heading", { name: "Files", exact: true }); + await expect(filesHeading).toBeVisible(); + + const resizeButton = page.locator('[data-direction="horizontal"]').first(); + await expect(resizeButton).toBeVisible(); + + const resizeButtonBox = await resizeButton.boundingBox(); + if (!resizeButtonBox) { + throw new Error("Resize button not found"); + } + + const initialX = resizeButtonBox.x; + + await page.mouse.move( + resizeButtonBox.x + resizeButtonBox.width / 2, + resizeButtonBox.y + resizeButtonBox.height / 2 + ); + await page.mouse.down(); + await page.mouse.move(resizeButtonBox.x + 100, resizeButtonBox.y + resizeButtonBox.height / 2); + await page.mouse.up(); + + await page.waitForTimeout(500); + + const deploymentsTab = page.getByRole("tab", { name: "deployments" }); + await deploymentsTab.click(); + await page.waitForTimeout(500); + + const explorerTab = page.getByRole("tab", { name: "explorer" }); + await explorerTab.click(); + await page.waitForTimeout(500); + + const resizeButtonAfterNav = page.locator('[data-direction="horizontal"]').first(); + await expect(resizeButtonAfterNav).toBeVisible(); + + const resizeButtonBoxAfterNav = await resizeButtonAfterNav.boundingBox(); + if (!resizeButtonBoxAfterNav) { + throw new Error("Resize button not found after navigation"); + } + + expect(Math.abs(resizeButtonBoxAfterNav.x - initialX)).toBeGreaterThan(50); + }); + + test("Should create and display file in project files", async ({ page }) => { + const filesHeading = page.getByRole("heading", { name: "Files", exact: true }); + await expect(filesHeading).toBeVisible(); + + await page.locator('button[aria-label="Create new file"]').click(); + await page.getByRole("textbox", { name: "new file name" }).click(); + await page.getByRole("textbox", { name: "new file name" }).fill("testFile.py"); + await page.getByRole("button", { name: "Create", exact: true }).click(); + + await page.waitForTimeout(1000); + + const fileInTree = page.getByText("testFile.py"); + await expect(fileInTree).toBeVisible(); + }); + + test("Should adjust split width when multiple files are added", async ({ page }) => { + const filesHeading = page.getByRole("heading", { name: "Files", exact: true }); + await expect(filesHeading).toBeVisible(); + + const resizeButton = page.locator('[data-direction="horizontal"]').first(); + await expect(resizeButton).toBeVisible(); + + const initialBox = await resizeButton.boundingBox(); + if (!initialBox) { + throw new Error("Resize button not found"); + } + + await page.locator('button[aria-label="Create new file"]').click(); + await page.getByRole("textbox", { name: "new file name" }).fill("veryLongFileNameThatShouldAdjustTheWidth.py"); + await page.getByRole("button", { name: "Create", exact: true }).click(); + + await page.waitForTimeout(1000); + + const fileInTree = page.getByText("veryLongFileNameThatShouldAdjustTheWidth.py"); + await expect(fileInTree).toBeVisible(); + + await page.waitForTimeout(500); + + const resizeButtonAfter = page.locator('[data-direction="horizontal"]').first(); + const boxAfter = await resizeButtonAfter.boundingBox(); + + if (boxAfter) { + expect(boxAfter.x).toBeGreaterThanOrEqual(initialBox.x); + } + }); +}); diff --git a/e2e/project/webhookSessionTriggered.spec.ts b/e2e/project/webhookSessionTriggered.spec.ts index edaffe8e9a..ca7ccaf4ba 100644 --- a/e2e/project/webhookSessionTriggered.spec.ts +++ b/e2e/project/webhookSessionTriggered.spec.ts @@ -1,42 +1,19 @@ import type { APIRequestContext, Page } from "@playwright/test"; -import randomatic from "randomatic"; import { expect, test } from "../fixtures"; -import { waitForToast } from "../utils"; -import { DashboardPage, ProjectPage } from "e2e/pages"; -import { waitForLoadingOverlayGone } from "e2e/utils/waitForLoadingOverlayToDisappear"; +import { ProjectPage, WebhookSessionPage } from "e2e/pages"; interface SetupParams { - dashboardPage: DashboardPage; page: Page; request: APIRequestContext; } -const projectName = `test_${randomatic("Aa", 4)}`; - -async function waitForFirstCompletedSession(page: Page, timeoutMs = 120000) { - await expect(async () => { - const refreshButton = page.locator('button[aria-label="Refresh"]'); - const isDisabled = await refreshButton.evaluate((element) => (element as HTMLButtonElement).disabled); - - if (!isDisabled) { - await refreshButton.click(); - } - - const completedSession = await page.getByRole("button", { name: "1 Completed" }).isVisible(); - - expect(completedSession).toBe(true); - - return true; - }).toPass({ - timeout: timeoutMs, - intervals: [3000], - }); -} - test.describe("Session triggered with webhook", () => { - test.beforeEach(async ({ dashboardPage, page, request }: SetupParams) => { - await setupProjectAndTriggerSession({ dashboardPage, page, request }); + let webhookSessionPage: WebhookSessionPage; + + test.beforeEach(async ({ page, request }: SetupParams) => { + webhookSessionPage = new WebhookSessionPage(page, request); + await webhookSessionPage.setupProjectAndTriggerSession(); }); test("Deploy project and execute session via webhook", async ({ @@ -46,7 +23,7 @@ test.describe("Session triggered with webhook", () => { page: Page; projectPage: ProjectPage; }) => { - test.setTimeout(5 * 60 * 1000); // 5 minutes + test.setTimeout(5 * 60 * 1000); const completedSessionDeploymentColumn = page.getByRole("button", { name: "1 Completed" }); await expect(completedSessionDeploymentColumn).toBeVisible(); @@ -57,69 +34,6 @@ test.describe("Session triggered with webhook", () => { await expect(sessionCompletedLog).toBeVisible(); await projectPage.stopDeployment(); - await projectPage.deleteProject(projectName); + await projectPage.deleteProject(webhookSessionPage.projectName); }); }); - -async function setupProjectAndTriggerSession({ dashboardPage, page, request }: SetupParams) { - await page.goto("/"); - - await page.getByRole("heading", { name: /^Welcome to .+$/, level: 1 }).isVisible(); - - try { - await page.locator('button[aria-label="Start From Template"]').click({ timeout: 3000 }); - - await expect(page.getByText("Start From Template")).toBeVisible(); - - await page.getByLabel("Categories").click(); - await page.getByRole("option", { name: "Samples" }).click(); - await page.locator("body").click({ position: { x: 0, y: 0 } }); - await page.locator('button[aria-label="Create Project From Template: HTTP"]').scrollIntoViewIfNeeded(); - await page.locator('button[aria-label="Create Project From Template: HTTP"]').click({ timeout: 2000 }); - - await page.getByPlaceholder("Enter project name").fill(projectName); - await page.locator('button[aria-label="Create"]').click(); - await page.waitForURL(/\/explorer\/settings/); - await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - await dashboardPage.createProjectFromTemplate(projectName); - } - - await waitForLoadingOverlayGone(page); - await page.locator('button[aria-label="Open Triggers Section"]').click(); - await expect(page.locator(`button[aria-label='Trigger information for "receive_http_get_or_head"']`)).toBeVisible(); - await page.locator(`button[aria-label='Trigger information for "receive_http_get_or_head"']`).hover(); - - const copyButton = await page.waitForSelector('[data-testid="copy-receive_http_get_or_head-webhook-url"]'); - const webhookUrl = await copyButton.getAttribute("value"); - - if (!webhookUrl) { - throw new Error("Failed to get webhook URL from button value attribute"); - } - - await page.locator('button[aria-label="Deploy project"]').click(); - - const toast = await waitForToast(page, "Project deployment completed successfully"); - await expect(toast).toBeVisible(); - - const response = await request.get(webhookUrl, { - timeout: 5000, - }); - - if (!response.ok()) { - throw new Error(`Webhook request failed with status ${response.status()}`); - } - - await page.locator('button[aria-label="Deployments"]').click(); - await expect(page.getByText("Deployment History")).toBeVisible(); - - await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible(); - await page.locator('button[aria-label="Close Project Settings"]').click(); - - await expect(page.getByText("Active").first()).toBeVisible(); - const deploymentId = page.getByText(/bld_*/); - await expect(deploymentId).toBeVisible(); - - await waitForFirstCompletedSession(page); -} diff --git a/playwright.config.ts b/playwright.config.ts index 303c7ce0f8..c1df6b8ddf 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -11,6 +11,12 @@ const extraHTTPHeaders: PlaywrightTestOptions["extraHTTPHeaders"] | undefined = ? { Authorization: `Bearer ${process.env.TESTS_JWT_AUTH_TOKEN}` } : {}; +const previewPort = process.env.VITE_PREVIEW_PORT + ? parseInt(process.env.VITE_PREVIEW_PORT) + : process.env.VITE_LOCAL_PORT + ? parseInt(process.env.VITE_LOCAL_PORT) + : 8000; + /** * See https://playwright.dev/docs/test-configuration. */ @@ -71,7 +77,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: "http://localhost:8000", + baseURL: `http://localhost:${previewPort}`, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "retain-on-failure", @@ -83,8 +89,8 @@ export default defineConfig({ }, webServer: { - command: "npm run build && npm run preview", - port: 8000, + command: `npm run build && npm run preview`, + port: previewPort, reuseExistingServer: !process.env.CI, stderr: "pipe", stdout: "pipe", diff --git a/src/assets/image/icons/Clock.svg b/src/assets/image/icons/Clock.svg index 4a2fe6bde1..f2317fc41e 100644 --- a/src/assets/image/icons/Clock.svg +++ b/src/assets/image/icons/Clock.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/assets/image/icons/Info.svg b/src/assets/image/icons/Info.svg index da1ec547fb..8a23c8cc95 100644 --- a/src/assets/image/icons/Info.svg +++ b/src/assets/image/icons/Info.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/src/assets/image/icons/InfoNoCircle.svg b/src/assets/image/icons/InfoNoCircle.svg new file mode 100644 index 0000000000..d621cfa901 --- /dev/null +++ b/src/assets/image/icons/InfoNoCircle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/image/icons/Link.svg b/src/assets/image/icons/Link.svg new file mode 100644 index 0000000000..933761452d --- /dev/null +++ b/src/assets/image/icons/Link.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/assets/image/icons/Webhook.svg b/src/assets/image/icons/Webhook.svg index 958d290985..16fadac843 100644 --- a/src/assets/image/icons/Webhook.svg +++ b/src/assets/image/icons/Webhook.svg @@ -1,4 +1,4 @@ - + diff --git a/src/assets/image/icons/index.ts b/src/assets/image/icons/index.ts index 2622a6d1aa..e9329dfb63 100644 --- a/src/assets/image/icons/index.ts +++ b/src/assets/image/icons/index.ts @@ -109,3 +109,7 @@ export { default as SendIcon } from "@assets/image/icons/Send.svg?react"; export { default as UploadIcon } from "@assets/image/icons/Upload.svg?react"; // Taken from: https://fontawesome.com/icons/toolbox?f=classic&s=solid export { default as EnvIcon } from "@assets/image/icons/Env.svg?react"; +// Taken from https://www.svgrepo.com/svg/437044/link +export { default as LinkIcon } from "@assets/image/icons/Link.svg?react"; +// Taken from: https://fontawesome.com/icons/info?f=classic&s=solid +export { default as InfoIconNoCircle } from "@assets/image/icons/InfoNoCircle.svg?react"; diff --git a/src/assets/image/icons/sidebar/Connections.svg b/src/assets/image/icons/sidebar/Connections.svg index 6a959c0ecc..4fd73446fe 100644 --- a/src/assets/image/icons/sidebar/Connections.svg +++ b/src/assets/image/icons/sidebar/Connections.svg @@ -1,3 +1,3 @@ - + diff --git a/src/autokitteh b/src/autokitteh index e0016389c8..c4a0c6f1bd 160000 --- a/src/autokitteh +++ b/src/autokitteh @@ -1 +1 @@ -Subproject commit e0016389c89e021c86aa0998bce4b28d5a404955 +Subproject commit c4a0c6f1bd4ed046b728b01b85b5771dc34c8785 diff --git a/src/components/atoms/icons/iconSvg.tsx b/src/components/atoms/icons/iconSvg.tsx index 26d320d279..9015f6191f 100644 --- a/src/components/atoms/icons/iconSvg.tsx +++ b/src/components/atoms/icons/iconSvg.tsx @@ -26,7 +26,7 @@ export const IconSvg = ({ const iconClasses = cn( "transition", { "hidden opacity-0": !isVisible, "opacity-40": disabled }, - { "rounded-full border border-gray-550 p-1": withCircle }, + { "rounded-full border border-gray-550 p-0.5": withCircle }, sizeClasses[size], className ); diff --git a/src/components/molecules/popover/popoverTrigger.tsx b/src/components/molecules/popover/popoverTrigger.tsx index 50fb6bf422..518b1269cf 100644 --- a/src/components/molecules/popover/popoverTrigger.tsx +++ b/src/components/molecules/popover/popoverTrigger.tsx @@ -6,11 +6,11 @@ import { usePopoverContext } from "@contexts"; import { PopoverTriggerProps } from "@src/interfaces/components"; export const PopoverTrigger = forwardRef & PopoverTriggerProps>( - function PopoverTrigger({ children, asChild, title, ...props }, propRef) { + function PopoverTrigger({ children, asChild, title, ariaLabel, ...props }, propRef) { const context = usePopoverContext(); const childrenRef = isValidElement(children) ? (children as any).ref : null; const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef].filter(Boolean)); - const ariaLabelProps = title ? { "aria-label": title } : {}; + const ariaLabelProps = ariaLabel ? { "aria-label": ariaLabel } : { "aria-label": title ?? "" }; const handleClick = () => { context.setOpen(!context.open); diff --git a/src/components/organisms/configuration/connections/integrations/google/add.tsx b/src/components/organisms/configuration/connections/integrations/google/add.tsx index 718c4645cb..e6bc48e325 100644 --- a/src/components/organisms/configuration/connections/integrations/google/add.tsx +++ b/src/components/organisms/configuration/connections/integrations/google/add.tsx @@ -46,8 +46,8 @@ export const GoogleIntegrationAddForm = ({ ); const configureConnection = async (connectionId: string) => { switch (connectionType?.value) { - case ConnectionAuthType.JsonKey: - await createConnection(connectionId, ConnectionAuthType.JsonKey, defaultGoogleConnectionName); + case ConnectionAuthType.Json: + await createConnection(connectionId, ConnectionAuthType.Json, defaultGoogleConnectionName); break; case ConnectionAuthType.Oauth: await handleCustomOauth(connectionId, defaultGoogleConnectionName); diff --git a/src/components/organisms/configuration/connections/integrations/google/edit.tsx b/src/components/organisms/configuration/connections/integrations/google/edit.tsx index 24c768c667..46f547fe40 100644 --- a/src/components/organisms/configuration/connections/integrations/google/edit.tsx +++ b/src/components/organisms/configuration/connections/integrations/google/edit.tsx @@ -15,7 +15,7 @@ export const GoogleIntegrationEditForm = ({ void; + showDeleteModal: (sessionId: string) => void; + selectedSessionId?: string; +} + +export const SessionInfoPopover = ({ + session, + onSessionRemoved, + showDeleteModal, + selectedSessionId, + className, +}: SessionInfoPopoverProps) => { + const { t } = useTranslation("deployments", { keyPrefix: "sessions.viewer" }); + const { t: tSessions } = useTranslation("deployments", { keyPrefix: "sessions" }); + const { t: tErrors } = useTranslation("errors"); + const addToast = useToastStore((state) => state.addToast); + const [isStopping, setIsStopping] = useState(false); + + const sourceType = session.memo?.trigger_source_type || t("manualRun"); + const sourceName = session.triggerName || session.connectionName || t("manualRun"); + const isDurable = session.memo?.is_durable === "true"; + const eventId = session.memo?.event_id; + + const handleDeleteClick = (event: React.MouseEvent) => { + event.stopPropagation(); + showDeleteModal(session.sessionId); + }; + + const handleStopSession = async (event: React.MouseEvent) => { + event.stopPropagation(); + if (session.state !== SessionState.running) return; + setIsStopping(true); + const { error } = await SessionsService.stop(session.sessionId); + setIsStopping(false); + if (error) { + addToast({ + message: tErrors("failedStopSession"), + type: "error", + }); + + return; + } + + addToast({ + message: tSessions("actions.sessionStoppedSuccessfully"), + type: "success", + }); + LoggerService.info( + namespaces.ui.sessions, + tSessions("actions.sessionStoppedSuccessfullyExtended", { sessionId: selectedSessionId }) + ); + + onSessionRemoved(); + }; + + const actionStoppedIconClass = + session.state === SessionState.running ? "h-4 w-4 transition group-hover:fill-white" : "h-4 w-4 transition"; + + return ( +
+ + + + + +
+
+
{t("startTime")}:
+
{dayjs(session.createdAt).format(dateTimeFormat)}
+
+ + {session.updatedAt ? ( +
+
{t("recentlyUpdated")}:
+
{dayjs(session.updatedAt).format(dateTimeFormat)}
+
+ ) : null} + +
+
{t("source")}:
+
+ {sourceType.toLowerCase()} + - + {sourceName} +
+
+ +
+
{t("entrypoint")}:
+
+ {session.entrypoint.path} + + {session.entrypoint.name} +
+
+ +
+
{t("status")}:
+ +
+ +
+
{t("isDurable")}:
+
{isDurable ? t("yes") : t("no")}
+
+ + {eventId ? ( +
+
{tSessions("table.eventId")}:
+
+
+ {eventId} +
+ +
+
+ ) : null} + + {session.deploymentId ? ( +
+
{tSessions("table.deploymentId")}:
+
+
+ {session.deploymentId} +
+ +
+
+ ) : null} + + {session.sessionId ? ( +
+
{tSessions("table.sessionId")}:
+
+
+ {session.sessionId} +
+ +
+
+ ) : null} + +
+ + {isStopping ? ( + + ) : ( + <> + {" "} + Stop + + )} + + + + Remove + +
+
+
+
+
+ ); +}; diff --git a/src/components/organisms/deployments/sessions/table/table.tsx b/src/components/organisms/deployments/sessions/table/table.tsx index d537abbc0f..21c2d6a1de 100644 --- a/src/components/organisms/deployments/sessions/table/table.tsx +++ b/src/components/organisms/deployments/sessions/table/table.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import { Outlet, useParams, useSearchParams } from "react-router-dom"; import { ListOnItemsRenderedProps } from "react-window"; -import { defaultSplitFrameSize, namespaces, tourStepsHTMLIds } from "@constants"; +import { defaultSessionsTableSplit, namespaces, tourStepsHTMLIds } from "@constants"; import { ModalName } from "@enums/components"; import { reverseSessionStateConverter } from "@models/utils"; import { LoggerService, SessionsService } from "@services"; @@ -59,18 +59,21 @@ export const SessionsTable = () => { const filteredEntityId = deploymentId || projectId!; const [searchParams, setSearchParams] = useSearchParams(); const [firstTimeLoading, setFirstTimeLoading] = useState(true); - const { splitScreenRatio, setEditorWidth, lastSeenSession, setLastSeenSession } = useSharedBetweenProjectsStore(); + const { sessionsTableSplit, setSessionsTableWidth, lastSeenSession, setLastSeenSession } = + useSharedBetweenProjectsStore(); const [leftSideWidth] = useResize({ direction: "horizontal", - ...defaultSplitFrameSize, - initial: splitScreenRatio[projectId!]?.sessions || defaultSplitFrameSize.initial, - value: splitScreenRatio[projectId!]?.sessions, + ...defaultSessionsTableSplit, + initial: sessionsTableSplit[projectId!] || defaultSessionsTableSplit.initial, + value: sessionsTableSplit[projectId!], id: resizeId, - onChange: (width) => setEditorWidth(projectId!, { sessions: width }), + onChange: (width) => setSessionsTableWidth(projectId!, width), }); const prevDeploymentsRef = useRef(deployments); - const isCompactMode = leftSideWidth < 20; + const isCompactMode = leftSideWidth < 25; + const hideSourceColumn = leftSideWidth < 35; + const hideActionsColumn = leftSideWidth < 27; const processStateFilter = (stateFilter?: string | null) => { if (!stateFilter) return ""; @@ -413,14 +416,24 @@ export const SessionsTable = () => { - - - - + + + {!hideSourceColumn ? ( + + ) : null} + {!hideActionsColumn ? ( + + ) : null} { - +
{sessionIdFromParams ? ( diff --git a/src/components/organisms/deployments/sessions/table/tableList.tsx b/src/components/organisms/deployments/sessions/table/tableList.tsx index 826b929a41..1afb75b4a3 100644 --- a/src/components/organisms/deployments/sessions/table/tableList.tsx +++ b/src/components/organisms/deployments/sessions/table/tableList.tsx @@ -17,6 +17,8 @@ export const SessionsTableList = ({ onSessionRemoved, sessions, openSession, + hideSourceColumn, + hideActionsColumn, }: SessionsTableListProps) => { const { sessionId } = useParams(); const { openModal } = useModalStore(); @@ -35,8 +37,10 @@ export const SessionsTableList = ({ selectedSessionId: sessionId, sessions, showDeleteModal, + hideSourceColumn, + hideActionsColumn, }), - [sessions, sessionId, openSession, showDeleteModal, onSessionRemoved] + [sessions, sessionId, openSession, showDeleteModal, onSessionRemoved, hideSourceColumn, hideActionsColumn] ); const rowRenderer: ListRowRenderer = ({ index, key, style }) => ( diff --git a/src/components/organisms/deployments/sessions/table/tableRow.tsx b/src/components/organisms/deployments/sessions/table/tableRow.tsx index 4897675368..0dfb1cfae4 100644 --- a/src/components/organisms/deployments/sessions/table/tableRow.tsx +++ b/src/components/organisms/deployments/sessions/table/tableRow.tsx @@ -7,14 +7,14 @@ import { SessionState } from "@enums"; import { SessionsTableRowProps } from "@interfaces/components"; import { LoggerService, SessionsService } from "@services"; import { dateTimeFormat, namespaces } from "@src/constants"; -import { cn } from "@utilities"; +import { cn, getSessionTriggerType } from "@utilities"; import { useToastStore } from "@store"; import { IconButton, Loader, Td, Tr } from "@components/atoms"; -import { SessionsTableState } from "@components/organisms/deployments"; +import { SessionsTableState, SessionInfoPopover } from "@components/organisms/deployments"; -import { ActionStoppedIcon, TrashIcon } from "@assets/image/icons"; +import { ActionStoppedIcon, TrashIcon, WebhookIcon, ClockIcon, PlayIcon, LinkIcon } from "@assets/image/icons"; const areEqual = ( prevProps: { data: SessionsTableRowProps; index: number }, @@ -26,6 +26,8 @@ const areEqual = ( return ( prevProps.index === nextProps.index && prevProps.data.selectedSessionId === nextProps.data.selectedSessionId && + prevProps.data.hideSourceColumn === nextProps.data.hideSourceColumn && + prevProps.data.hideActionsColumn === nextProps.data.hideActionsColumn && prevSession === nextSession ); }; @@ -35,19 +37,46 @@ export const SessionsTableRow = memo( const { t: tErrors } = useTranslation("errors"); const { t } = useTranslation("deployments", { keyPrefix: "sessions" }); const addToast = useToastStore((state) => state.addToast); - const { onSessionRemoved, openSession, selectedSessionId, sessions, showDeleteModal } = data; + const { + onSessionRemoved, + openSession, + selectedSessionId, + sessions, + showDeleteModal, + hideSourceColumn, + hideActionsColumn, + } = data; const session = sessions[index]; const [isStopping, setIsStopping] = useState(false); if (!session) { return null; } + const triggerType = getSessionTriggerType(session.memo); const sessionRowClass = (id: string) => - cn("group flex cursor-pointer items-center hover:bg-gray-1300", { + cn("group flex cursor-pointer items-center fill-white hover:bg-gray-1300", { "bg-black": id === selectedSessionId, }); + const renderTriggerIcon = () => { + const iconClassName = cn("size-4 shrink-0", { + "fill-white stroke-white": triggerType === "manual", + }); + switch (triggerType) { + case "webhook": + return ; + case "schedule": + return ; + case "connection": + return ; + case "manual": + return ; + default: + return null; + } + }; + const handleDeleteClick = (event: React.MouseEvent) => { event.stopPropagation(); showDeleteModal(session.sessionId); @@ -90,35 +119,65 @@ export const SessionsTableRow = memo( onClick={() => openSession(session.sessionId)} style={{ ...style }} > -
- - - - + {renderTriggerIcon()} {sessionTriggerName} + + + )} + + {!hideActionsColumn ? ( + + ) : null} ); }, diff --git a/src/components/organisms/files/projectFiles.tsx b/src/components/organisms/files/projectFiles.tsx index bbc6153edf..cff32f11f5 100644 --- a/src/components/organisms/files/projectFiles.tsx +++ b/src/components/organisms/files/projectFiles.tsx @@ -20,7 +20,7 @@ export const ProjectFiles = () => { const { resources } = useCacheStore(); const { openFileAsActive, openFiles } = useFileStore(); const { openModal, closeModal, getModalData } = useModalStore(); - const { setIsProjectFilesVisible, setEditorWidth } = useSharedBetweenProjectsStore(); + const { setIsProjectFilesVisible, setProjectSplitScreenWidth } = useSharedBetweenProjectsStore(); const addToast = useToastStore((state) => state.addToast); const { fetchResources } = useCacheStore(); const { closeOpenedFile } = useFileStore(); @@ -157,7 +157,7 @@ export const ProjectFiles = () => { useEffect(() => { if (projectId && !!files?.length) { const optimalWidth = calculateOptimalSplitFrameWidth(Object.keys(resources || {}), 35, 15); - setEditorWidth(projectId, { explorer: optimalWidth }); + setProjectSplitScreenWidth(projectId, optimalWidth); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [files, projectId]); diff --git a/src/components/organisms/splitFrame.tsx b/src/components/organisms/splitFrame.tsx index 5963d32e7c..8cbb37dea0 100644 --- a/src/components/organisms/splitFrame.tsx +++ b/src/components/organisms/splitFrame.tsx @@ -15,7 +15,8 @@ import { EditorTabs } from "@components/organisms"; export const SplitFrame = ({ children, rightFrameClass: rightBoxClass }: SplitFrameProps) => { const resizeHorizontalId = useId(); - const { splitScreenRatio, setEditorWidth, isProjectFilesVisible } = useSharedBetweenProjectsStore(); + const { projectSplitScreenWidth, setProjectSplitScreenWidth, isProjectFilesVisible } = + useSharedBetweenProjectsStore(); const { projectId } = useParams(); const { pathname } = useLocation(); const { activeTour } = useTourStore(); @@ -23,10 +24,10 @@ export const SplitFrame = ({ children, rightFrameClass: rightBoxClass }: SplitFr const [leftSideWidth] = useResize({ direction: "horizontal", ...defaultSplitFrameSize, - initial: splitScreenRatio[projectId!]?.explorer || defaultSplitFrameSize.initial, - value: splitScreenRatio[projectId!]?.explorer, + initial: projectSplitScreenWidth[projectId!] || defaultSplitFrameSize.initial, + value: projectSplitScreenWidth[projectId!], id: resizeHorizontalId, - onChange: (width) => setEditorWidth(projectId!, { explorer: width }), + onChange: (width) => setProjectSplitScreenWidth(projectId!, width), }); const shouldShowProjectFiles = !!isProjectFilesVisible[projectId!]; diff --git a/src/constants/connections/formsPerIntegrationsMapping.constants.ts b/src/constants/connections/formsPerIntegrationsMapping.constants.ts index fbb8461140..c95207033a 100644 --- a/src/constants/connections/formsPerIntegrationsMapping.constants.ts +++ b/src/constants/connections/formsPerIntegrationsMapping.constants.ts @@ -80,7 +80,7 @@ export const formsPerIntegrationsMapping: Partial< }, [Integrations.gmail]: { [ConnectionAuthType.Oauth]: OauthGoogleForm, - [ConnectionAuthType.JsonKey]: JsonKeyGoogleForm, + [ConnectionAuthType.Json]: JsonKeyGoogleForm, }, [Integrations.sheets]: { [ConnectionAuthType.Oauth]: OauthGoogleForm, diff --git a/src/constants/connections/integrationVariablesMapping.constants.ts b/src/constants/connections/integrationVariablesMapping.constants.ts index e44042c066..f763173787 100644 --- a/src/constants/connections/integrationVariablesMapping.constants.ts +++ b/src/constants/connections/integrationVariablesMapping.constants.ts @@ -28,7 +28,7 @@ export const integrationVariablesMapping = { auth_token: "Password", }, [Integrations.gmail]: { - json: "JSON", + json: "json", }, [Integrations.sheets]: { json: "JSON", diff --git a/src/constants/index.ts b/src/constants/index.ts index 6b926e6871..04ad3b681d 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -73,6 +73,7 @@ export { export { defaultSystemLogSize, defaultSplitFrameSize, + defaultSessionsTableSplit, defaultChatbotWidth, defaultProjectSettingsWidth, } from "@constants/resize.constants"; diff --git a/src/constants/lists/connections/options.constants.ts b/src/constants/lists/connections/options.constants.ts index b38bbb9ca1..5d7380dc2f 100644 --- a/src/constants/lists/connections/options.constants.ts +++ b/src/constants/lists/connections/options.constants.ts @@ -49,7 +49,7 @@ export const zoomIntegrationAuthMethods: SelectOption[] = [ export const selectIntegrationGoogle: SelectOption[] = [ { label: "User (OAuth 2.0)", value: ConnectionAuthType.Oauth }, - { label: "Service Account (JSON Key)", value: ConnectionAuthType.JsonKey }, + { label: "Service Account (JSON Key)", value: ConnectionAuthType.Json }, ]; const slackSocketMode = { label: "Socket Mode", value: ConnectionAuthType.Socket }; diff --git a/src/constants/resize.constants.ts b/src/constants/resize.constants.ts index 7ef92e386e..a8d6d00768 100644 --- a/src/constants/resize.constants.ts +++ b/src/constants/resize.constants.ts @@ -34,5 +34,11 @@ export const defaultChatbotWidth = { export const defaultProjectSettingsWidth = { max: 80, min: 20, + initial: 20, +}; + +export const defaultSessionsTableSplit = { + max: 80, + min: 22, initial: 35, }; diff --git a/src/hooks/useConnectionForm.ts b/src/hooks/useConnectionForm.ts index 8588c28f53..62da83bdd0 100644 --- a/src/hooks/useConnectionForm.ts +++ b/src/hooks/useConnectionForm.ts @@ -162,11 +162,11 @@ export const useConnectionForm = (validationSchema: ZodSchema, mode: FormMode, a formSchema as ZodObject | ZodEffects, integrationName ); - await HttpService.post( `/${formattedIntegrationName}/save?cid=${connectionId}&origin=web&auth_type=${connectionAuthType}`, connectionData ); + addToast({ message: t("connectionCreateSuccess"), type: "success", @@ -343,6 +343,13 @@ export const useConnectionForm = (validationSchema: ZodSchema, mode: FormMode, a }; const onSubmit = async () => { + console.log("onSubmit"); + console.log("connectionId", connectionId); + console.log("errors", errors); + console.log("getValues", getValues()); + console.log("formSchema", formSchema); + console.log("mode", mode); + console.log("authOptions", authOptions); if (connectionId) { const connId = connectionId; setConnectionId(undefined); diff --git a/src/interfaces/components/popover.interface.ts b/src/interfaces/components/popover.interface.ts index 9329f5ff27..2119652dcb 100644 --- a/src/interfaces/components/popover.interface.ts +++ b/src/interfaces/components/popover.interface.ts @@ -21,6 +21,8 @@ export interface PopoverOptions { export interface PopoverTriggerProps { children: React.ReactNode; asChild?: boolean; + ariaLabel?: string; + title?: string; onClick?: (event: React.MouseEvent) => void; } diff --git a/src/interfaces/components/session.interface.ts b/src/interfaces/components/session.interface.ts index 4cfa8cbbc6..d45b78a14f 100644 --- a/src/interfaces/components/session.interface.ts +++ b/src/interfaces/components/session.interface.ts @@ -18,6 +18,8 @@ export interface SessionsTableRowProps { sessions: Session[]; showDeleteModal: (id: string) => void; onSessionRemoved: () => void; + hideSourceColumn?: boolean; + hideActionsColumn?: boolean; } export interface SessionsTableListProps { @@ -26,4 +28,6 @@ export interface SessionsTableListProps { onSessionRemoved: () => void; sessions: Session[]; openSession: (sessionId: string) => void; + hideSourceColumn?: boolean; + hideActionsColumn?: boolean; } diff --git a/src/interfaces/models/session.interface.ts b/src/interfaces/models/session.interface.ts index 667a7ef879..a736090452 100644 --- a/src/interfaces/models/session.interface.ts +++ b/src/interfaces/models/session.interface.ts @@ -23,6 +23,7 @@ export interface BuildRuntimeExport extends SessionEntrypoint { interface BaseSession { createdAt: Date; + updatedAt?: Date; inputs: Record; memo: Record; sessionId: string; diff --git a/src/interfaces/store/sharedBetweenProjectsStore.interface.ts b/src/interfaces/store/sharedBetweenProjectsStore.interface.ts index 087ebbdc00..bc96c8a530 100644 --- a/src/interfaces/store/sharedBetweenProjectsStore.interface.ts +++ b/src/interfaces/store/sharedBetweenProjectsStore.interface.ts @@ -21,8 +21,10 @@ export interface SharedBetweenProjectsStore { fullScreenEditor: { [projectId: string]: boolean }; setFullScreenEditor: (projectId: string, value: boolean) => void; expandedProjectNavigation: { [projectId: string]: boolean }; - splitScreenRatio: Record; - setEditorWidth: (projectId: string, { explorer, sessions }: { explorer?: number; sessions?: number }) => void; + projectSplitScreenWidth: { [projectId: string]: number }; + setProjectSplitScreenWidth: (projectId: string, width: number) => void; + sessionsTableSplit: { [projectId: string]: number }; + setSessionsTableWidth: (projectId: string, width: number) => void; chatbotWidth: { [projectId: string]: number }; setChatbotWidth: (projectId: string, width: number) => void; projectSettingsWidth: { [projectId: string]: number }; diff --git a/src/locales/en/deployments/translation.json b/src/locales/en/deployments/translation.json index 1df8a23870..fb314a44e4 100644 --- a/src/locales/en/deployments/translation.json +++ b/src/locales/en/deployments/translation.json @@ -81,6 +81,7 @@ "error": "Stopped due to error" }, "sessions": { + "ariaLabelAdditionalInformation": "Additional information session: {{sessionId}}", "ariaLabelReturnBack": "Return back", "buttons": { "back": "Back", @@ -96,6 +97,12 @@ "sessionsRefreshed": "Sessions refreshed successfully", "sessionsStillOld": "Sessions are still old", "table": { + "deploymentId": "Deployment ID", + "sessionId": "Session ID", + "eventId": "Event ID", + "ariaLabelDeploymentId": "Deployment ID: {{deploymentId}}", + "ariaLabelSessionId": "Session ID: {{sessionId}}", + "ariaLabelEventId": "Event ID: {{eventId}}", "columns": { "actions": "Actions", "startTime": "Start time", @@ -156,7 +163,12 @@ "outputLogsFetchError": "Couldn't fetch the output logs", "noActivitiesFound": "No activites found", "errorFetchingActivities": "Error fetching activities", - "noLogsFound": "No logs found" + "noLogsFound": "No logs found", + "startTime": "Start Time", + "recentlyUpdated": "Recently Updated", + "isDurable": "Is Durable", + "yes": "Yes", + "no": "No" }, "actions": { "sessionStoppedSuccessfully": "Session request successfully sent", @@ -202,4 +214,4 @@ }, "unnamedActivity": "Unnamed activity" } -} +} \ No newline at end of file diff --git a/src/models/session.model.ts b/src/models/session.model.ts index 38c6deb0fe..8af1fdd2e3 100644 --- a/src/models/session.model.ts +++ b/src/models/session.model.ts @@ -13,6 +13,7 @@ function convertProtoSessionBase(protoSession: ProtoSession) { state: protoSession.state, triggerName: protoSession.memo?.trigger_name, entrypoint: { ...protoSession.entrypoint }, + updatedAt: convertTimestampToDate(protoSession.updatedAt), }; } @@ -34,6 +35,5 @@ export function convertSessionProtoToViewerModel(protoSession: ProtoSession): Vi buildId: protoSession.buildId, eventId: protoSession.eventId, sourceType: protoSession.memo?.trigger_source_type || t("sessions.viewer.manualRun", { ns: "deployments" }), - updatedAt: convertTimestampToDate(protoSession.updatedAt!), } as ViewerSession; } diff --git a/src/store/useSharedBetweenProjectsStore.ts b/src/store/useSharedBetweenProjectsStore.ts index 8775211fff..b78c7304cf 100644 --- a/src/store/useSharedBetweenProjectsStore.ts +++ b/src/store/useSharedBetweenProjectsStore.ts @@ -10,7 +10,7 @@ const defaultState: Omit< | "setCursorPosition" | "setSelection" | "setFullScreenEditor" - | "setEditorWidth" + | "setProjectSplitScreenWidth" | "setFullScreenDashboard" | "setIsChatbotFullScreen" | "setChatbotWidth" @@ -24,13 +24,15 @@ const defaultState: Omit< | "setDrawerAnimated" | "setLastVisitedUrl" | "setLastSeenSession" + | "setSessionsTableWidth" > = { cursorPositionPerProject: {}, selectionPerProject: {}, fullScreenEditor: {}, expandedProjectNavigation: {}, fullScreenDashboard: false, - splitScreenRatio: {}, + projectSplitScreenWidth: {}, + sessionsTableSplit: {}, chatbotWidth: {}, projectSettingsWidth: {}, projectFilesWidth: {}, @@ -78,17 +80,17 @@ const store: StateCreator = (set) => ({ return state; }), - setEditorWidth: (projectId, { explorer, sessions }) => { - set(({ splitScreenRatio }) => ({ - splitScreenRatio: { - ...splitScreenRatio, - [projectId]: { - explorer: explorer !== undefined ? explorer : splitScreenRatio[projectId]?.explorer, - sessions: sessions !== undefined ? sessions : splitScreenRatio[projectId]?.sessions, - }, - }, - })); - }, + setProjectSplitScreenWidth: (projectId: string, width: number) => + set((state) => { + state.projectSplitScreenWidth[projectId] = width; + return state; + }), + + setSessionsTableWidth: (projectId: string, width: number) => + set((state) => { + state.sessionsTableSplit[projectId] = width; + return state; + }), setChatbotWidth: (projectId: string, width: number) => set((state) => { @@ -187,7 +189,7 @@ const store: StateCreator = (set) => ({ export const useSharedBetweenProjectsStore = create( persist(immer(store), { name: StoreName.sharedBetweenProjects, - version: 12, + version: 13, migrate: (persistedState, version) => { let migratedState = persistedState; @@ -291,6 +293,29 @@ export const useSharedBetweenProjectsStore = create( }); } + // Version 13: Migrate splitScreenRatio to projectSplitScreenWidth + if (version < 13 && migratedState) { + const splitScreenRatio = (migratedState as any).splitScreenRatio; + const projectSplitScreenWidth: { [key: string]: number } = {}; + + if (splitScreenRatio && typeof splitScreenRatio === "object") { + for (const [projectId, ratio] of Object.entries(splitScreenRatio)) { + if (ratio && typeof ratio === "object" && "explorer" in ratio) { + projectSplitScreenWidth[projectId] = (ratio as any).explorer; + } + } + } + + migratedState = { + ...migratedState, + projectSplitScreenWidth, + }; + + if ((migratedState as any).splitScreenRatio) { + delete (migratedState as any).splitScreenRatio; + } + } + return migratedState; }, partialize: (state) => { diff --git a/src/utilities/getSessionTriggerType.utils.ts b/src/utilities/getSessionTriggerType.utils.ts new file mode 100644 index 0000000000..687b378d55 --- /dev/null +++ b/src/utilities/getSessionTriggerType.utils.ts @@ -0,0 +1,23 @@ +export type SessionTriggerType = "webhook" | "schedule" | "connection" | "manual"; + +export const getSessionTriggerType = (memo: Record): SessionTriggerType => { + if (!memo || Object.keys(memo).length === 0) { + return "manual"; + } + + const triggerSourceType = memo.trigger_source_type; + + if (triggerSourceType === "WEBHOOK") { + return "webhook"; + } + + if (triggerSourceType === "SCHEDULE") { + return "schedule"; + } + + if (triggerSourceType === "CONNECTION") { + return "connection"; + } + + return "manual"; +}; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 199de1ddc8..6bab4058aa 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -86,3 +86,5 @@ export { getTriggersWithBadConnections } from "@utilities/projectValidation.util export { generateItemIds } from "@utilities/generateItemIds.utils"; export { buildCodeFixData, validateCodeFixSuggestion } from "@utilities/codeFixData.utility"; export { processBulkCodeFixSuggestions, generateBulkCodeFixSummary } from "@utilities/bulkCodeFix.utility"; +export { getSessionTriggerType } from "@utilities/getSessionTriggerType.utils"; +export type { SessionTriggerType } from "@utilities/getSessionTriggerType.utils"; diff --git a/src/validations/connection.schema.ts b/src/validations/connection.schema.ts index 9a7827f5fc..bbe02342d4 100644 --- a/src/validations/connection.schema.ts +++ b/src/validations/connection.schema.ts @@ -42,11 +42,11 @@ export const googleJsonIntegrationSchema = z.object({ .or(z.literal(Integrations.sheets)) .or(z.literal(Integrations.drive)) .or(z.literal(Integrations.youtube)), - auth_type: z.literal(ConnectionAuthType.JsonKey).default(ConnectionAuthType.JsonKey), + auth_type: z.literal(ConnectionAuthType.Json).default(ConnectionAuthType.Json), }); export const googleCalendarIntegrationSchema = z.object({ - json: z.string().min(1, "Json Key is required").optional(), + jsonKey: z.string().min(1, "Json Key is required").optional(), cal_id: z.string().optional(), auth_scopes: z.literal(`google${Integrations.calendar}`).default(`google${Integrations.calendar}`), auth_type: z @@ -56,7 +56,7 @@ export const googleCalendarIntegrationSchema = z.object({ }); export const googleFormsIntegrationSchema = z.object({ - json: z.string().min(1, "Json Key is required").optional(), + jsonKey: z.string().min(1, "Json Key is required").optional(), form_id: z.string().optional(), auth_scopes: z.literal(`google${Integrations.forms}`).default(`google${Integrations.forms}`), auth_type: z diff --git a/vite.config.ts b/vite.config.ts index dd6f2413b0..ea287927c8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -23,7 +23,11 @@ export default defineConfig({ publicDir: path.resolve(__dirname, "public"), clearScreen: false, preview: { - port: 8000, + port: process.env.VITE_PREVIEW_PORT + ? parseInt(process.env.VITE_PREVIEW_PORT) + : process.env.VITE_LOCAL_PORT + ? parseInt(process.env.VITE_LOCAL_PORT) + : 8000, }, build: { sourcemap: true, From 9dc27d52f6f74a4060c0971b75c2487845052bc4 Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Wed, 19 Nov 2025 14:47:02 +0200 Subject: [PATCH 3/8] fix: sessions table refactor --- e2e/pages/dashboard.ts | 5 +- e2e/pages/project.ts | 2 - e2e/pages/webhookSession.ts | 9 +- e2e/project/sessionsCompactMode.spec.ts | 161 +++++------------- .../dashboard/templates/tabs/card.tsx | 6 +- .../deployments/sessions/table/table.tsx | 4 +- .../deployments/sessions/table/tableRow.tsx | 15 +- src/components/pages/aiLandingPage.tsx | 2 + 8 files changed, 67 insertions(+), 137 deletions(-) diff --git a/e2e/pages/dashboard.ts b/e2e/pages/dashboard.ts index 11cd2a5c7c..0663d2379b 100644 --- a/e2e/pages/dashboard.ts +++ b/e2e/pages/dashboard.ts @@ -52,7 +52,10 @@ export class DashboardPage { } async createProjectFromTemplate(projectName: string) { - await this.page.goto("/"); + await this.page.goto("/welcome"); + await this.page.getByRole("button", { name: "Start from Template" }).hover(); + await this.page.getByRole("button", { name: "Start from Template" }).click(); + await this.page.getByLabel("Categories").click(); await this.page.getByRole("option", { name: "Samples" }).click(); await this.page.locator("body").click({ position: { x: 0, y: 0 } }); diff --git a/e2e/pages/project.ts b/e2e/pages/project.ts index 5274173ea9..d78820c0cd 100644 --- a/e2e/pages/project.ts +++ b/e2e/pages/project.ts @@ -21,8 +21,6 @@ export class ProjectPage { const loadersArray = await loaders; await Promise.all(loadersArray.map((loader) => loader.waitFor({ state: "detached" }))); - await this.page.getByRole("heading", { name: /^Welcome to .+$/, level: 1 }).isVisible(); - await expect(successToast).not.toBeVisible({ timeout: 10000 }); const deletedProjectNameCell = this.page.getByRole("cell", { name: projectName }); diff --git a/e2e/pages/webhookSession.ts b/e2e/pages/webhookSession.ts index 33d71b275e..f06ff1131f 100644 --- a/e2e/pages/webhookSession.ts +++ b/e2e/pages/webhookSession.ts @@ -39,14 +39,13 @@ export class WebhookSessionPage { } async setupProjectAndTriggerSession() { - await this.page.goto("/"); - - await this.page.getByRole("heading", { name: /^Welcome to .+$/, level: 1 }).isVisible(); + await this.page.goto("/welcome"); try { - await this.page.locator('button[aria-label="Start From Template"]').click({ timeout: 3000 }); + await this.page.locator('button[aria-label="Start from Template"]').hover(); + await this.page.locator('button[aria-label="Start from Template"]').click(); - await expect(this.page.getByText("Start From Template")).toBeVisible(); + await expect(this.page.locator('h2[aria-label="Start from Template"]')).toBeVisible(); await this.page.getByLabel("Categories").click(); await this.page.getByRole("option", { name: "Samples" }).click(); diff --git a/e2e/project/sessionsCompactMode.spec.ts b/e2e/project/sessionsCompactMode.spec.ts index 5c726b0de2..5eb759a882 100644 --- a/e2e/project/sessionsCompactMode.spec.ts +++ b/e2e/project/sessionsCompactMode.spec.ts @@ -25,6 +25,17 @@ test.describe("Sessions Table Compact Mode Suite", () => { const sessionsTableFrame = page.locator("#sessions-table"); await expect(sessionsTableFrame).toBeVisible(); + const sessionTriggerTypeIconInSourceColumn = page.getByRole("img", { + name: "webhook receive_http_get_or_head trigger icon", + exact: true, + }); + await expect(sessionTriggerTypeIconInSourceColumn).toBeVisible(); + const sessionTriggerTypeIconInStartTimeColumn = page.getByRole("img", { + name: "webhook receive_http_get_or_head trigger", + exact: true, + }); + await expect(sessionTriggerTypeIconInStartTimeColumn).not.toBeVisible(); + const resizeButton = page.locator("#sessions-table-resize-button"); await expect(resizeButton).toBeVisible(); @@ -50,17 +61,22 @@ test.describe("Sessions Table Compact Mode Suite", () => { await page.waitForTimeout(500); - const startTimeCell = sessionsTableFrame.locator('td[aria-label^="Session "]').first(); - const infoButtons = startTimeCell.locator('button[aria-label^="Additional information session"]'); - const hasIcon = await infoButtons.count(); - expect(hasIcon).toBeGreaterThan(0); + const sessionTriggerNameCell = page.getByRole("cell", { + name: "receive_http_get_or_head", + exact: true, + }); + await expect(sessionTriggerNameCell).not.toBeVisible(); + const sessionTriggerTypeIconInStartColumn = page.getByRole("img", { + name: "webhook receive_http_get_or_head trigger", + exact: true, + }); + await expect(sessionTriggerTypeIconInStartColumn).toBeVisible(); }); test("Should display text when sessions table is not in compact mode", async ({ page }) => { test.setTimeout(5 * 60 * 1000); - const sessionsButton = page.locator('button[aria-label="Sessions"]'); - await sessionsButton.click(); + await page.getByRole("button", { name: "Sessions" }).click(); await page.waitForTimeout(2000); @@ -92,18 +108,17 @@ test.describe("Sessions Table Compact Mode Suite", () => { await page.waitForTimeout(500); - const sourceColumnCell = sessionsTableFrame.locator('td[aria-label^="Session "]').first(); - const cellText = await sourceColumnCell.textContent(); + const sessionTriggerNameCell = page.getByRole("cell", { name: "receive_http_get_or_head", exact: true }); + await expect(sessionTriggerNameCell).toBeVisible(); + const sessionTriggerTypeIcon = page.getByRole("img", { name: "webhook receive_http_get_or_head trigger icon" }); - expect(cellText).toBeTruthy(); - expect(cellText?.trim().length).toBeGreaterThan(0); + await expect(sessionTriggerTypeIcon).toBeVisible(); }); test("Should toggle between icon and text when resizing", async ({ page }) => { test.setTimeout(5 * 60 * 1000); - const sessionsButton = page.locator('button[aria-label="Sessions"]'); - await sessionsButton.click(); + await page.getByRole("button", { name: "Sessions" }).click(); await page.waitForTimeout(2000); @@ -134,16 +149,15 @@ test.describe("Sessions Table Compact Mode Suite", () => { await page.waitForTimeout(500); - const sourceColumnCell = sessionsTableFrame.locator('td[aria-label^="Session "]').first(); - const initialText = await sourceColumnCell.textContent(); - expect(initialText?.trim().length).toBeGreaterThan(0); + const sessionTriggerNameCell = page.getByRole("cell", { name: "receive_http_get_or_head", exact: true }); + await expect(sessionTriggerNameCell).toBeVisible(); const newResizeButtonBox = await resizeButton.boundingBox(); if (!newResizeButtonBox) { throw new Error("Resize button not found after first resize"); } - const narrowTargetX = viewportSize.width * 0.15; + const narrowTargetX = viewportSize.width * 0.2; await page.mouse.move( newResizeButtonBox.x + newResizeButtonBox.width / 2, newResizeButtonBox.y + newResizeButtonBox.height / 2 @@ -154,110 +168,15 @@ test.describe("Sessions Table Compact Mode Suite", () => { await page.waitForTimeout(500); - const hasIcon = await sourceColumnCell.locator('button[aria-label^="Additional information session"]').count(); - expect(hasIcon).toBeGreaterThan(0); - }); - - test("Should persist compact mode setting after navigation", async ({ page }) => { - test.setTimeout(5 * 60 * 1000); - - const sessionsButton = page.locator('button[aria-label="Sessions"]'); - await sessionsButton.click(); - - await page.waitForTimeout(2000); - - const sessionsTableFrame = page.locator("#sessions-table"); - await expect(sessionsTableFrame).toBeVisible(); - - const resizeButton = page.locator("#sessions-table-resize-button"); - await expect(resizeButton).toBeVisible(); - - const resizeButtonBox = await resizeButton.boundingBox(); - if (!resizeButtonBox) { - throw new Error("Resize button not found"); - } - - const viewportSize = page.viewportSize(); - if (!viewportSize) { - throw new Error("Viewport size not available"); - } - - const targetX = viewportSize.width * 0.15; - - await page.mouse.move( - resizeButtonBox.x + resizeButtonBox.width / 2, - resizeButtonBox.y + resizeButtonBox.height / 2 - ); - await page.mouse.down(); - await page.mouse.move(targetX, resizeButtonBox.y + resizeButtonBox.height / 2); - await page.mouse.up(); - - await page.waitForTimeout(500); - - let sourceColumnCell = sessionsTableFrame.locator('td[aria-label^="Session "]').first(); - const hasIconBefore = await sourceColumnCell - .locator('button[aria-label^="Additional information session"]') - .count(); - expect(hasIconBefore).toBeGreaterThan(0); - - const deploymentsButton = page.locator('button[aria-label="Deployments"]'); - await deploymentsButton.click(); - await page.waitForTimeout(500); - - await sessionsButton.click(); - await page.waitForTimeout(500); - - sourceColumnCell = sessionsTableFrame.locator('td[aria-label^="Session "]').first(); - const hasIconAfter = await sourceColumnCell - .locator('button[aria-label^="Additional information session"]') - .count(); - expect(hasIconAfter).toBeGreaterThan(0); - }); - - test("Should display Webhook icon for webhook sessions in compact mode", async ({ page }) => { - test.setTimeout(5 * 60 * 1000); - - const sessionsButton = page.locator('button[aria-label="Sessions"]'); - await sessionsButton.click(); - - await page.waitForTimeout(2000); - - const sessionsTableFrame = page.locator("#sessions-table"); - await expect(sessionsTableFrame).toBeVisible(); - - const resizeButton = page.locator("#sessions-table-resize-button"); - await expect(resizeButton).toBeVisible(); - - const resizeButtonBox = await resizeButton.boundingBox(); - if (!resizeButtonBox) { - throw new Error("Resize button not found"); - } - - const viewportSize = page.viewportSize(); - if (!viewportSize) { - throw new Error("Viewport size not available"); - } - - const targetX = viewportSize.width * 0.15; - - await page.mouse.move( - resizeButtonBox.x + resizeButtonBox.width / 2, - resizeButtonBox.y + resizeButtonBox.height / 2 - ); - await page.mouse.down(); - await page.mouse.move(targetX, resizeButtonBox.y + resizeButtonBox.height / 2); - await page.mouse.up(); - - await page.waitForTimeout(500); - - const iconButton = sessionsTableFrame - .locator('td[aria-label^="Session "] button[aria-label^="Additional information session"]') - .first(); - const icon = iconButton.locator("svg").first(); - - await expect(iconButton).toBeVisible(); - - const iconClass = await icon.getAttribute("class"); - expect(iconClass).toContain("h-4 w-4"); + const sessionTriggerNameCellAfterResize = page.getByRole("img", { + name: "webhook receive_http_get_or_head trigger", + exact: true, + }); + await expect(sessionTriggerNameCellAfterResize).toBeVisible(); + const hasIconInStartTimeColumn = page.getByRole("img", { + name: "webhook receive_http_get_or_head trigger icon", + exact: true, + }); + await expect(hasIconInStartTimeColumn).not.toBeVisible(); }); }); diff --git a/src/components/organisms/dashboard/templates/tabs/card.tsx b/src/components/organisms/dashboard/templates/tabs/card.tsx index 13948bbcf8..f3d92dd4b5 100644 --- a/src/components/organisms/dashboard/templates/tabs/card.tsx +++ b/src/components/organisms/dashboard/templates/tabs/card.tsx @@ -41,7 +41,11 @@ export const ProjectTemplateCard = ({ {template.description} -
+
{t("start")} diff --git a/src/components/organisms/deployments/sessions/table/table.tsx b/src/components/organisms/deployments/sessions/table/table.tsx index 21c2d6a1de..4bc2e28c20 100644 --- a/src/components/organisms/deployments/sessions/table/table.tsx +++ b/src/components/organisms/deployments/sessions/table/table.tsx @@ -363,9 +363,9 @@ export const SessionsTable = () => { }; return ( -
+
- +
diff --git a/src/components/organisms/deployments/sessions/table/tableRow.tsx b/src/components/organisms/deployments/sessions/table/tableRow.tsx index 0dfb1cfae4..38891cc6df 100644 --- a/src/components/organisms/deployments/sessions/table/tableRow.tsx +++ b/src/components/organisms/deployments/sessions/table/tableRow.tsx @@ -132,9 +132,11 @@ export const SessionsTableRow = memo( session={session} showDeleteModal={showDeleteModal} /> -
- {hideSourceColumn ? renderTriggerIcon() : null} -
+ {hideSourceColumn ? ( +
+ renderTriggerIcon() +
+ ) : null}
@@ -145,14 +147,17 @@ export const SessionsTableRow = memo( {hideSourceColumn ? null : (
)} diff --git a/src/components/pages/aiLandingPage.tsx b/src/components/pages/aiLandingPage.tsx index 349473089b..6eed68f13c 100644 --- a/src/components/pages/aiLandingPage.tsx +++ b/src/components/pages/aiLandingPage.tsx @@ -146,8 +146,10 @@ export const AiLandingPage = () => { ) : null} From 0da808ac22250243d5c047ec57b1f5a9e2fbea97 Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Wed, 19 Nov 2025 15:05:07 +0200 Subject: [PATCH 4/8] fix: sessions table refactor --- e2e/project/splitScreen.spec.ts | 47 +++++++++++++++---- src/assets/image/icons/Info.svg | 2 +- src/components/atoms/buttons/resizeButton.tsx | 4 +- .../organisms/files/projectFiles.tsx | 7 ++- src/components/organisms/splitFrame.tsx | 8 +++- src/components/pages/project.tsx | 2 + src/hooks/useConnectionForm.ts | 7 --- .../components/resizeButton.interface.ts | 1 + 8 files changed, 56 insertions(+), 22 deletions(-) diff --git a/e2e/project/splitScreen.spec.ts b/e2e/project/splitScreen.spec.ts index 51b77847d8..48ba065627 100644 --- a/e2e/project/splitScreen.spec.ts +++ b/e2e/project/splitScreen.spec.ts @@ -30,7 +30,7 @@ test.describe("Split Screen Suite", () => { const filesHeading = page.getByRole("heading", { name: "Files", exact: true }); await expect(filesHeading).toBeVisible(); - const resizeButton = page.locator('[data-direction="horizontal"]').first(); + const resizeButton = page.locator('[data-testid="split-frame-resize-button"]'); await expect(resizeButton).toBeVisible(); const resizeButtonBox = await resizeButton.boundingBox(); @@ -53,7 +53,15 @@ test.describe("Split Screen Suite", () => { const filesHeading = page.getByRole("heading", { name: "Files", exact: true }); await expect(filesHeading).toBeVisible(); - const resizeButton = page.locator('[data-direction="horizontal"]').first(); + const splitFrame = page.locator("#project-split-frame"); + await expect(splitFrame).toBeVisible(); + + const splitFrameBoxBefore = await splitFrame.boundingBox(); + if (!splitFrameBoxBefore) { + throw new Error("Split frame not found"); + } + + const resizeButton = page.locator('[data-testid="split-frame-resize-button"]'); await expect(resizeButton).toBeVisible(); const resizeButtonBox = await resizeButton.boundingBox(); @@ -61,7 +69,7 @@ test.describe("Split Screen Suite", () => { throw new Error("Resize button not found"); } - const initialX = resizeButtonBox.x; + const initialPercentage = ((resizeButtonBox.x - splitFrameBoxBefore.x) / splitFrameBoxBefore.width) * 100; await page.mouse.move( resizeButtonBox.x + resizeButtonBox.width / 2, @@ -71,17 +79,25 @@ test.describe("Split Screen Suite", () => { await page.mouse.move(resizeButtonBox.x + 100, resizeButtonBox.y + resizeButtonBox.height / 2); await page.mouse.up(); + const resizedButtonBox = await resizeButton.boundingBox(); + if (!resizedButtonBox) { + throw new Error("Resize button not found after drag"); + } + + const resizedPercentage = ((resizedButtonBox.x - splitFrameBoxBefore.x) / splitFrameBoxBefore.width) * 100; + expect(Math.abs(resizedPercentage - initialPercentage)).toBeGreaterThan(2); + await page.waitForTimeout(500); - const deploymentsTab = page.getByRole("tab", { name: "deployments" }); + const deploymentsTab = page.getByRole("button", { name: "Deployments" }); await deploymentsTab.click(); await page.waitForTimeout(500); - const explorerTab = page.getByRole("tab", { name: "explorer" }); + const explorerTab = page.getByRole("button", { name: "Explorer" }); await explorerTab.click(); await page.waitForTimeout(500); - const resizeButtonAfterNav = page.locator('[data-direction="horizontal"]').first(); + const resizeButtonAfterNav = page.locator('[data-testid="split-frame-resize-button"]'); await expect(resizeButtonAfterNav).toBeVisible(); const resizeButtonBoxAfterNav = await resizeButtonAfterNav.boundingBox(); @@ -89,13 +105,24 @@ test.describe("Split Screen Suite", () => { throw new Error("Resize button not found after navigation"); } - expect(Math.abs(resizeButtonBoxAfterNav.x - initialX)).toBeGreaterThan(50); + const splitFrameBoxAfterNav = await splitFrame.boundingBox(); + if (!splitFrameBoxAfterNav) { + throw new Error("Split frame not found after navigation"); + } + + const resizePercentageAfterNav = + ((resizeButtonBoxAfterNav.x - splitFrameBoxAfterNav.x) / splitFrameBoxAfterNav.width) * 100; + + expect(Math.abs(resizePercentageAfterNav - resizedPercentage)).toBeLessThan(0.5); }); test("Should create and display file in project files", async ({ page }) => { const filesHeading = page.getByRole("heading", { name: "Files", exact: true }); await expect(filesHeading).toBeVisible(); + const projectFilesTreeContainer = page.locator('[data-testid="project-files-tree-container"]'); + await expect(projectFilesTreeContainer).toBeVisible(); + await page.locator('button[aria-label="Create new file"]').click(); await page.getByRole("textbox", { name: "new file name" }).click(); await page.getByRole("textbox", { name: "new file name" }).fill("testFile.py"); @@ -103,7 +130,7 @@ test.describe("Split Screen Suite", () => { await page.waitForTimeout(1000); - const fileInTree = page.getByText("testFile.py"); + const fileInTree = projectFilesTreeContainer.getByText("testFile.py"); await expect(fileInTree).toBeVisible(); }); @@ -111,7 +138,7 @@ test.describe("Split Screen Suite", () => { const filesHeading = page.getByRole("heading", { name: "Files", exact: true }); await expect(filesHeading).toBeVisible(); - const resizeButton = page.locator('[data-direction="horizontal"]').first(); + const resizeButton = page.locator('[data-testid="split-frame-resize-button"]'); await expect(resizeButton).toBeVisible(); const initialBox = await resizeButton.boundingBox(); @@ -130,7 +157,7 @@ test.describe("Split Screen Suite", () => { await page.waitForTimeout(500); - const resizeButtonAfter = page.locator('[data-direction="horizontal"]').first(); + const resizeButtonAfter = page.locator('[data-testid="split-frame-resize-button"]'); const boxAfter = await resizeButtonAfter.boundingBox(); if (boxAfter) { diff --git a/src/assets/image/icons/Info.svg b/src/assets/image/icons/Info.svg index 8a23c8cc95..da1ec547fb 100644 --- a/src/assets/image/icons/Info.svg +++ b/src/assets/image/icons/Info.svg @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/src/components/atoms/buttons/resizeButton.tsx b/src/components/atoms/buttons/resizeButton.tsx index ffad3795a9..71dc030f50 100644 --- a/src/components/atoms/buttons/resizeButton.tsx +++ b/src/components/atoms/buttons/resizeButton.tsx @@ -3,7 +3,7 @@ import React from "react"; import { ResizeButtonProps } from "@interfaces/components"; import { cn } from "@utilities"; -export const ResizeButton = ({ className, direction, resizeId, id }: ResizeButtonProps) => { +export const ResizeButton = ({ className, direction, resizeId, id, dataTestId }: ResizeButtonProps) => { const isVertical = direction === "vertical"; const buttonResizeClasses = cn( @@ -15,5 +15,5 @@ export const ResizeButton = ({ className, direction, resizeId, id }: ResizeButto className ); - return
; + return
; }; diff --git a/src/components/organisms/files/projectFiles.tsx b/src/components/organisms/files/projectFiles.tsx index cff32f11f5..7df3e5f51c 100644 --- a/src/components/organisms/files/projectFiles.tsx +++ b/src/components/organisms/files/projectFiles.tsx @@ -180,7 +180,12 @@ export const ProjectFiles = () => {
-
+
: null} - + ) : null} diff --git a/src/components/pages/project.tsx b/src/components/pages/project.tsx index 5aec2ef77c..f5c3ad4723 100644 --- a/src/components/pages/project.tsx +++ b/src/components/pages/project.tsx @@ -64,6 +64,8 @@ export const Project = () => {
{t("table.columns.startTime")}{t("table.columns.status")}{t("table.columns.source")}{t("table.columns.actions")} + {t("table.columns.startTime")} + {t("table.columns.status")}{t("table.columns.source")}{t("table.columns.actions")}
{dayjs(session.createdAt).format(dateTimeFormat)} - + +
+ {dayjs(session.createdAt).format(dateTimeFormat)} + +
+ {hideSourceColumn ? renderTriggerIcon() : null} +
+
- {sessionTriggerName} + + -
- +
- {isStopping ? ( - - ) : ( - - )} - - - - - -
-
+
+ + {isStopping ? ( + + ) : ( + + )} + + + + + +
+
+
+ {renderTriggerIcon()} +
- {renderTriggerIcon()} {sessionTriggerName} + {sessionTriggerName}