diff --git a/.github/workflows/build_test_and_release.yml b/.github/workflows/build_test_and_release.yml index 9e835c61be..3aa07d84f4 100644 --- a/.github/workflows/build_test_and_release.yml +++ b/.github/workflows/build_test_and_release.yml @@ -75,7 +75,7 @@ jobs: needs: setup runs-on: ubuntu-latest if: github.ref != 'refs/heads/main' - timeout-minutes: 30 + timeout-minutes: 40 strategy: matrix: browser: ["Chrome", "Edge", "Firefox", "Safari"] diff --git a/e2e/pages/dashboard.ts b/e2e/pages/dashboard.ts index 11cd2a5c7c..541f1c1871 100644 --- a/e2e/pages/dashboard.ts +++ b/e2e/pages/dashboard.ts @@ -52,13 +52,17 @@ 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 } }); await this.page.getByRole("button", { name: "Create Project From Template: HTTP" }).scrollIntoViewIfNeeded(); await this.page.getByRole("button", { name: "Create Project From Template: HTTP" }).click(); await this.page.getByPlaceholder("Enter project name").fill(projectName); + await this.page.waitForTimeout(500); await this.page.getByRole("button", { name: "Create", exact: true }).click(); await expect(this.page.getByRole("heading", { name: "Configuration" })).toBeVisible({ timeout: 1200 }); } 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/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 new file mode 100644 index 0000000000..552f747289 --- /dev/null +++ b/e2e/pages/webhookSession.ts @@ -0,0 +1,106 @@ +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("/welcome"); + + try { + 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.locator('h2[aria-label="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.waitForTimeout(500); + + 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..5eb759a882 --- /dev/null +++ b/e2e/project/sessionsCompactMode.spec.ts @@ -0,0 +1,182 @@ +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 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(); + + 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 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); + + await page.getByRole("button", { name: "Sessions" }).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 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" }); + + await expect(sessionTriggerTypeIcon).toBeVisible(); + }); + + test("Should toggle between icon and text when resizing", async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + await page.getByRole("button", { name: "Sessions" }).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 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.2; + 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 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/e2e/project/splitScreen.spec.ts b/e2e/project/splitScreen.spec.ts new file mode 100644 index 0000000000..30ff9e8a0a --- /dev/null +++ b/e2e/project/splitScreen.spec.ts @@ -0,0 +1,163 @@ +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-testid="split-frame-resize-button"]'); + 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 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(); + if (!resizeButtonBox) { + throw new Error("Resize button not found"); + } + const initialPercentage = ((resizeButtonBox.x - splitFrameBoxBefore.x) / splitFrameBoxBefore.width) * 100; + 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(); + + 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("button", { name: "Deployments" }); + await deploymentsTab.click(); + await page.waitForTimeout(500); + + const explorerTab = page.getByRole("button", { name: "Explorer" }); + await explorerTab.click(); + await page.waitForTimeout(500); + + const resizeButtonAfterNav = page.locator('[data-testid="split-frame-resize-button"]'); + await expect(resizeButtonAfterNav).toBeVisible(); + + const resizeButtonBoxAfterNav = await resizeButtonAfterNav.boundingBox(); + if (!resizeButtonBoxAfterNav) { + throw new Error("Resize button not found after navigation"); + } + + 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"); + await page.getByRole("button", { name: "Create", exact: true }).click(); + + await page.waitForTimeout(1000); + + const fileInTree = projectFilesTreeContainer.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-testid="split-frame-resize-button"]'); + 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-testid="split-frame-resize-button"]'); + 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/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/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/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 = ({ -
+
{t("start")} diff --git a/src/components/organisms/deployments/index.ts b/src/components/organisms/deployments/index.ts index 992d362869..ce4be7f46b 100644 --- a/src/components/organisms/deployments/index.ts +++ b/src/components/organisms/deployments/index.ts @@ -5,5 +5,6 @@ export { SessionsTable, SessionsTableFilter, SessionsTableState, + SessionInfoPopover, } from "@components/organisms/deployments/sessions"; export { DeploymentsTableContent } from "@components/organisms/deployments/tableContent"; diff --git a/src/components/organisms/deployments/sessions/index.ts b/src/components/organisms/deployments/sessions/index.ts index 6b0d58ff7e..89367269be 100644 --- a/src/components/organisms/deployments/sessions/index.ts +++ b/src/components/organisms/deployments/sessions/index.ts @@ -4,4 +4,5 @@ export { SessionsTableState } from "@components/organisms/deployments/sessions/t export { SessionsTable } from "@components/organisms/deployments/sessions/table/table"; export { SessionsTableList } from "@components/organisms/deployments/sessions/table/tableList"; export { SessionsTableRow } from "@components/organisms/deployments/sessions/table/tableRow"; +export { SessionInfoPopover } from "@components/organisms/deployments/sessions/table/sessionInfoPopover"; export { SessionViewer } from "@components/organisms/deployments/sessions/viewer"; 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/sessionInfoPopover.tsx b/src/components/organisms/deployments/sessions/table/sessionInfoPopover.tsx new file mode 100644 index 0000000000..c48e4bd7bc --- /dev/null +++ b/src/components/organisms/deployments/sessions/table/sessionInfoPopover.tsx @@ -0,0 +1,240 @@ +import React, { useState } from "react"; + +import dayjs from "dayjs"; +import { useTranslation } from "react-i18next"; + +import { dateTimeFormat, namespaces } from "@constants"; +import { SessionState } from "@enums"; +import { LoggerService, SessionsService } from "@services"; +import { Session } from "@src/interfaces/models"; + +import { useToastStore } from "@store"; + +import { IconButton, IconSvg, Loader } from "@components/atoms"; +import { CopyButton } from "@components/molecules"; +import { PopoverWrapper, PopoverTrigger, PopoverContent } from "@components/molecules/popover"; +import { SessionsTableState } from "@components/organisms/deployments"; + +import { ArrowRightIcon, ActionStoppedIcon, TrashIcon, InfoIconNoCircle } from "@assets/image/icons"; + +interface SessionInfoPopoverProps { + className?: string; + session: Session; + onSessionRemoved: () => 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 ddc0f53054..4bc2e28c20 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,17 +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 < 25; + const hideSourceColumn = leftSideWidth < 35; + const hideActionsColumn = leftSideWidth < 27; const processStateFilter = (stateFilter?: string | null) => { if (!stateFilter) return ""; @@ -359,9 +363,9 @@ export const SessionsTable = () => { }; return ( -
+
- +
@@ -390,6 +394,7 @@ export const SessionsTable = () => {
navigateInSessions(sessionIdFromParams || "", sessionState)} selectedState={urlSessionStateFilter} /> @@ -411,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..38891cc6df 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,70 @@ export const SessionsTableRow = memo( onClick={() => openSession(session.sessionId)} style={{ ...style }} > -
- - - - + {sessionTriggerName} + + + )} + + {!hideActionsColumn ? ( + + ) : null} ); }, diff --git a/src/components/organisms/files/projectFiles.tsx b/src/components/organisms/files/projectFiles.tsx index bbc6153edf..b812e03c69 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(); @@ -156,8 +156,11 @@ export const ProjectFiles = () => { useEffect(() => { if (projectId && !!files?.length) { - const optimalWidth = calculateOptimalSplitFrameWidth(Object.keys(resources || {}), 35, 15); - setEditorWidth(projectId, { explorer: optimalWidth }); + const { projectSplitScreenWidth: storedWidth } = useSharedBetweenProjectsStore.getState(); + if (!storedWidth[projectId]) { + const optimalWidth = calculateOptimalSplitFrameWidth(Object.keys(resources || {}), 35, 15); + setProjectSplitScreenWidth(projectId, optimalWidth); + } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [files, projectId]); @@ -180,7 +183,12 @@ export const ProjectFiles = () => {
-
+
{ 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!]; @@ -69,7 +70,13 @@ export const SplitFrame = ({ children, rightFrameClass: rightBoxClass }: SplitFr ) : null} {isConnectionTourActive ?
: null} - + ) : 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} 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} + + -
- +
+ {renderTriggerIcon()} +
+
- {isStopping ? ( - - ) : ( - - )} - - - - - -
-
+
+ + {isStopping ? ( + + ) : ( + + )} + + + + + +
+