diff --git a/backend/package-lock.json b/backend/package-lock.json index 6c2085b0377..4b1403a93b6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -102,6 +102,7 @@ "knex": "^3.0.1", "ldapjs": "^3.0.7", "ldif": "0.5.1", + "libpg-query": "^17.7.3", "libsodium-wrappers": "^0.7.13", "lodash.isequal": "^4.5.0", "mongodb": "^6.8.1", @@ -126,6 +127,7 @@ "passport-ldapauth": "^3.0.1", "passport-oauth2": "^1.8.0", "pg": "^8.11.3", + "pg-cursor": "^2.19.0", "pg-query-stream": "^4.5.3", "pg-types": "^2.2.0", "picomatch": "^3.0.1", @@ -5681,9 +5683,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5701,9 +5700,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5721,9 +5717,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5741,9 +5734,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5761,9 +5751,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5781,9 +5768,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5801,9 +5785,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5821,9 +5802,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5841,9 +5819,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5867,9 +5842,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5893,9 +5865,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5919,9 +5888,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5945,9 +5911,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5971,9 +5934,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -5997,9 +5957,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -6023,9 +5980,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -7131,9 +7085,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7151,9 +7102,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7171,9 +7119,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7191,9 +7136,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8866,6 +8808,12 @@ "tsyringe": "^4.10.0" } }, + "node_modules/@pgsql/types": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/@pgsql/types/-/types-17.6.2.tgz", + "integrity": "sha512-1UtbELdbqNdyOShhrVfSz3a1gDi0s9XXiQemx+6QqtsrXe62a6zOGU+vjb2GRfG5jeEokI1zBBcfD42enRv0Rw==", + "license": "MIT" + }, "node_modules/@phc/format": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", @@ -9945,9 +9893,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -9962,9 +9907,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -9979,9 +9921,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -9996,9 +9935,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10013,9 +9949,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10030,9 +9963,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10047,9 +9977,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10064,9 +9991,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10081,9 +10005,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10098,9 +10019,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -10115,9 +10033,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10132,9 +10047,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -10149,9 +10061,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -19419,6 +19328,15 @@ "node": ">= 0.8.0" } }, + "node_modules/libpg-query": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/libpg-query/-/libpg-query-17.7.3.tgz", + "integrity": "sha512-lHKBvoWRsXt/9bJxpAeFxkLu0CA6tELusqy3o1z6/DwGXSETxhKJDaNlNdrNV8msvXDLBhpg/4RE/fKKs5rYFA==", + "license": "MIT", + "dependencies": { + "@pgsql/types": "^17.6.2" + } + }, "node_modules/libsodium": { "version": "0.7.13", "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.13.tgz", @@ -21962,9 +21880,10 @@ "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" }, "node_modules/pg-cursor": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.10.3.tgz", - "integrity": "sha512-rDyBVoqPVnx/PTmnwQAYgusSeAKlTL++gmpf5klVK+mYMFEqsOc6VHHZnPKc/4lOvr4r6fiMuoxSFuBF1dx4FQ==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.19.0.tgz", + "integrity": "sha512-J5cF1MUz7LRJ9emOqF/06QjabMHMZy587rSPF0UuA8rCwKeeYl2co8Pp+6k5UU9YrAYHMzWkLxilfZB0hqsWWw==", + "license": "MIT", "peerDependencies": { "pg": "^8" } diff --git a/backend/package.json b/backend/package.json index f598a02d5f0..d61682cf783 100644 --- a/backend/package.json +++ b/backend/package.json @@ -224,6 +224,7 @@ "knex": "^3.0.1", "ldapjs": "^3.0.7", "ldif": "0.5.1", + "libpg-query": "^17.7.3", "libsodium-wrappers": "^0.7.13", "lodash.isequal": "^4.5.0", "mongodb": "^6.8.1", @@ -248,6 +249,7 @@ "passport-ldapauth": "^3.0.1", "passport-oauth2": "^1.8.0", "pg": "^8.11.3", + "pg-cursor": "^2.19.0", "pg-query-stream": "^4.5.3", "pg-types": "^2.2.0", "picomatch": "^3.0.1", diff --git a/backend/src/@types/pg-cursor.d.ts b/backend/src/@types/pg-cursor.d.ts new file mode 100644 index 00000000000..11415e9fae7 --- /dev/null +++ b/backend/src/@types/pg-cursor.d.ts @@ -0,0 +1,27 @@ +declare module "pg-cursor" { + import type { Connection } from "pg"; + + interface CursorConfig { + types?: { + getTypeParser: (oid: number) => (val: string | Buffer) => unknown; + }; + } + + class Cursor = Record> { + // Internal result object populated after read() completes. + // eslint-disable-next-line @typescript-eslint/naming-convention + _result: { + fields: { name: string }[]; + command: string | null; + rowCount: number | null; + }; + + constructor(text: string, values?: unknown[] | null, config?: CursorConfig); + + submit(connection: Connection): void; + read(rows: number): Promise; + close(): Promise; + } + + export = Cursor; +} diff --git a/backend/src/ee/services/pam-web-access/pam-postgres-session-handler.test.ts b/backend/src/ee/services/pam-web-access/pam-postgres-session-handler.test.ts index c5070c3b74d..5a4bf5635eb 100644 --- a/backend/src/ee/services/pam-web-access/pam-postgres-session-handler.test.ts +++ b/backend/src/ee/services/pam-web-access/pam-postgres-session-handler.test.ts @@ -30,7 +30,7 @@ vi.mock("./pam-web-access-repl", () => ({ // Mock pg vi.mock("pg", () => { - const mockQuery = vi.fn(); + const mockQuery = vi.fn().mockResolvedValue({ rows: [] }); const mockConnect = vi.fn().mockResolvedValue(undefined); const mockEnd = vi.fn().mockResolvedValue(undefined); const mockOn = vi.fn(); diff --git a/backend/src/ee/services/pam-web-access/pam-postgres-session-handler.ts b/backend/src/ee/services/pam-web-access/pam-postgres-session-handler.ts index d60d1277ed7..43d3d89c22e 100644 --- a/backend/src/ee/services/pam-web-access/pam-postgres-session-handler.ts +++ b/backend/src/ee/services/pam-web-access/pam-postgres-session-handler.ts @@ -1,4 +1,6 @@ +import { parse as parseSql } from "libpg-query"; import pg from "pg"; +import Cursor from "pg-cursor"; import { TPostgresAccountCredentials, @@ -35,6 +37,22 @@ export const handlePostgresSession = async ( ctx; const { connectionDetails, credentials } = params; + // Type parser shared between the pg.Client and pg-cursor instances so all + // results — whether fetched via the simple query path or cursor — apply the + // same normalisation rules. + const pgTypes = { + getTypeParser: (oid: number) => { + // Boolean (OID 16): Postgres wire protocol sends 't'/'f' — expand to 'true'/'false' + // so the Data Explorer UI displays human-readable literals. + if (oid === 16) + return (val: string | Buffer) => { + const raw = typeof val === "string" ? val : val.toString("utf8"); + return raw === "t" ? "true" : "false"; + }; + return (val: string | Buffer) => (typeof val === "string" ? val : val.toString("hex")); + } + }; + const pgClient = new pg.Client({ host: "localhost", port: relayPort, @@ -44,22 +62,14 @@ export const handlePostgresSession = async ( ssl: false, connectionTimeoutMillis: 30_000, statement_timeout: 30_000, - types: { - getTypeParser: (oid: number) => { - // Boolean (OID 16): Postgres wire protocol sends 't'/'f' — expand to 'true'/'false' - // so the Data Explorer UI displays human-readable literals. - if (oid === 16) - return (val: string | Buffer) => { - const raw = typeof val === "string" ? val : val.toString("utf8"); - return raw === "t" ? "true" : "false"; - }; - return (val: string | Buffer) => (typeof val === "string" ? val : val.toString("hex")); - } - } + types: pgTypes }); await pgClient.connect(); + const { rows: pidRows } = await pgClient.query<{ pid: number }>("SELECT pg_backend_pid() AS pid"); + const backendPid = pidRows[0]?.pid; + const repl = createPamSqlRepl(pgClient); sendMessage({ @@ -80,6 +90,10 @@ export const handlePostgresSession = async ( } }; + // Server-side transaction state — updated after every query so the client + // always receives the authoritative value, including for multi-statement SQL. + let isInTransaction = false; + // Shared error handler for correlated query messages const sendQueryError = async (id: string, err: unknown) => { const pgErr = err as { message?: string; detail?: string; hint?: string }; @@ -92,6 +106,8 @@ export const handlePostgresSession = async ( // ROLLBACK fails if there was no active transaction — safe to ignore. } + isInTransaction = false; + sendResponse({ type: PostgresServerMessageType.Error, id, @@ -101,13 +117,45 @@ export const handlePostgresSession = async ( }); }; + // Cancel the currently running query via pg_cancel_backend. + // Runs on a separate connection so it is not blocked by the sequential queue. + const cancelRunningQuery = async () => { + if (!backendPid) return; + const pid = backendPid; + const cancelClient = new pg.Client({ + host: "localhost", + port: relayPort, + user: credentials.username, + database: connectionDetails.database, + password: "", + ssl: false, + connectionTimeoutMillis: 5_000 + }); + try { + await cancelClient.connect(); + await cancelClient.query("SELECT pg_cancel_backend($1)", [pid]); + } catch (err) { + logger.debug(err, "Failed to cancel backend query"); + } finally { + await cancelClient.end().catch(() => {}); + } + }; + // Sequential message processing to prevent concurrent query issues let processingPromise = Promise.resolve(); socket.on("message", (rawData: Buffer | ArrayBuffer | Buffer[]) => { + const message = parseClientMessage(rawData, PostgresClientMessageSchema); + + // Cancel is handled immediately outside the sequential queue so it can + // interrupt a running query rather than waiting behind it. + if (message?.type === PostgresClientMessageType.Cancel) { + void cancelRunningQuery(); + return; + } + processingPromise = processingPromise .then(async () => { - const message = parseClientMessage(rawData, PostgresClientMessageSchema); if (!message) { sendMessage({ type: TerminalServerMessageType.Output, @@ -193,19 +241,61 @@ export const handlePostgresSession = async ( case PostgresClientMessageType.Query: { try { - // Multi-statement SQL (transactions) is executed via PostgreSQL's simple query - // protocol, which returns only the last statement's result. For transaction-wrapped - // batches (BEGIN;...;COMMIT;), the result is from COMMIT (no rows/fields). const startTime = performance.now(); - const result = await pgClient.query(message.sql); + const MAX_ROWS = 1000; + + // Split the SQL into individual statements using the real PostgreSQL C parser + // (via libpg-query WASM). Each statement is then run through a pg-cursor with + // an explicit row cap — the server sends at most MAX_ROWS+1 rows at the wire + // level regardless of result set size, so memory usage is bounded. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const parsed = await parseSql(message.sql); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const stmtTexts = (parsed.stmts as Array<{ stmt_location?: number; stmt_len?: number }>).map((s) => { + const location = s.stmt_location ?? 0; + return s.stmt_len !== undefined + ? message.sql.slice(location, location + s.stmt_len) + : message.sql.slice(location).trim(); + }); + + let lastRows: Record[] = []; + let lastFields: { name: string }[] = []; + let lastRowCount: number | null = null; + let lastCommand = ""; + let lastIsTruncated = false; + + for (const stmtSql of stmtTexts) { + const cursor = pgClient.query(new Cursor(stmtSql.trim(), null, { types: pgTypes })); + // eslint-disable-next-line no-await-in-loop + const stmtRows = await cursor.read(MAX_ROWS + 1); + const stmtIsTruncated = stmtRows.length > MAX_ROWS; + if (stmtIsTruncated) stmtRows.splice(MAX_ROWS); + // eslint-disable-next-line no-await-in-loop + await cursor.close(); + + // eslint-disable-next-line no-underscore-dangle + const cursorResult = cursor._result; + const cmd = (cursorResult.command ?? "").toUpperCase(); + if (cmd === "BEGIN" || cmd === "START") isInTransaction = true; + if (cmd === "COMMIT" || cmd === "ROLLBACK") isInTransaction = false; + + lastRows = stmtRows; + lastFields = (cursorResult.fields ?? []).map((f) => ({ name: f.name })); + lastRowCount = cursorResult.rowCount; + lastCommand = cursorResult.command ?? ""; + lastIsTruncated = stmtIsTruncated; + } + const executionTimeMs = Math.round(performance.now() - startTime); sendResponse({ type: PostgresServerMessageType.QueryResult, id: message.id, - rows: (result.rows ?? []) as Record[], - fields: (result.fields ?? []).map((f) => ({ name: f.name })), - rowCount: result.rowCount, - command: result.command ?? "", + rows: lastRows, + fields: lastFields, + rowCount: lastRowCount, + isTruncated: lastIsTruncated, + transactionOpen: isInTransaction, + command: lastCommand, executionTimeMs }); } catch (err) { diff --git a/backend/src/ee/services/pam-web-access/pam-postgres-ws-types.ts b/backend/src/ee/services/pam-web-access/pam-postgres-ws-types.ts index c88208b5b97..bda4395a9e0 100644 --- a/backend/src/ee/services/pam-web-access/pam-postgres-ws-types.ts +++ b/backend/src/ee/services/pam-web-access/pam-postgres-ws-types.ts @@ -8,7 +8,8 @@ export enum PostgresClientMessageType { GetSchemas = "get-schemas", GetTables = "get-tables", GetTableDetail = "get-table-detail", - Query = "query" + Query = "query", + Cancel = "cancel" } export enum PostgresServerMessageType { @@ -54,13 +55,16 @@ const QueryRequestSchema = CorrelatedBaseSchema.extend({ sql: z.string().max(50 * 1024) }); +const CancelSchema = z.object({ type: z.literal(PostgresClientMessageType.Cancel) }); + export const PostgresClientMessageSchema = z.discriminatedUnion("type", [ InputSchema, ControlSchema, GetSchemasRequestSchema, GetTablesRequestSchema, GetTableDetailRequestSchema, - QueryRequestSchema + QueryRequestSchema, + CancelSchema ]); export type TPostgresClientMessage = z.infer; @@ -135,6 +139,8 @@ const QueryResultResponseSchema = CorrelatedBaseSchema.extend({ }) ), rowCount: z.number().nullable(), + isTruncated: z.boolean(), + transactionOpen: z.boolean(), command: z.string(), executionTimeMs: z.number() }); diff --git a/docs/documentation/platform/pam/product-reference/web-access/overview.mdx b/docs/documentation/platform/pam/product-reference/web-access/overview.mdx index 23c02697fbe..a5fc06fcaba 100644 --- a/docs/documentation/platform/pam/product-reference/web-access/overview.mdx +++ b/docs/documentation/platform/pam/product-reference/web-access/overview.mdx @@ -1,13 +1,13 @@ --- title: "Web Access" sidebarTitle: "Overview" -description: "Access PAM-managed resources directly from your browser with an interactive terminal." +description: "Access PAM-managed resources directly from your browser." --- Infisical PAM Web Access provides a browser-based portal for interacting with your infrastructure directly from your browser via the Infisical dashboard — no client tools or CLI required. - Web Access currently supports **PostgreSQL** databases (with a visual Data Explorer and SQL terminal), **Redis** servers, and **SSH** servers. + Web Access currently supports **PostgreSQL** databases (with visual table browsing, inline editing, and a SQL query runner), **Redis** servers, and **SSH** servers. ## How It Works @@ -46,7 +46,7 @@ Sessions that exceed the idle timeout or max duration are automatically terminat - Visual Data Explorer for browsing and editing tables, plus an interactive SQL terminal. + Visual table browsing and editing, plus an integrated SQL query runner. Interactive Redis REPL with redis-cli-style output formatting. diff --git a/docs/documentation/platform/pam/product-reference/web-access/postgresql.mdx b/docs/documentation/platform/pam/product-reference/web-access/postgresql.mdx index c28a52e70dd..920bac0b6ee 100644 --- a/docs/documentation/platform/pam/product-reference/web-access/postgresql.mdx +++ b/docs/documentation/platform/pam/product-reference/web-access/postgresql.mdx @@ -6,13 +6,9 @@ description: "Browse, query, and edit PostgreSQL databases directly from your br ![PostgreSQL Web Access](/images/pam/product-reference/web-access/pg-data-explorer-overview.png) -PostgreSQL web access provides two modes for interacting with your database directly from the browser: the **Data Explorer** for visual browsing and editing, and the **SQL Terminal** for running ad-hoc queries. +PostgreSQL web access provides a unified interface for interacting with your database directly from the browser. It combines visual table browsing and editing with an integrated SQL query runner, all in one interface. -## Data Explorer - -The Data Explorer is a visual interface for browsing and editing PostgreSQL tables. It provides schema and table navigation, filtering, sorting, inline editing, and full CRUD operations — all without writing SQL. - -### Connecting +## Connecting @@ -25,10 +21,10 @@ The Data Explorer is a visual interface for browsing and editing PostgreSQL tabl ![Account Page Access Button](/images/pam/product-reference/web-access/pg-account-page-access-button.png) - - In the connect modal, click **Open Data Explorer** to launch the Data Explorer in a new tab. + + In the connect modal, click **Connect in Browser** to launch the web access interface in a new tab. - ![Open Data Explorer](/images/pam/product-reference/web-access/pg-data-explorer-connect-button.png) + ![Connect in Browser](/images/pam/product-reference/web-access/pg-data-explorer-connect-button.png) @@ -38,65 +34,29 @@ Once connected, you get a spreadsheet-like interface for your database where you - Filter and sort to quickly find what you need - Edit data directly — add, update, or delete rows -![Data Explorer](/images/pam/product-reference/web-access/pg-data-explorer-overview.png) - -## SQL Terminal +![PostgreSQL Web Access Interface](/images/pam/product-reference/web-access/pg-data-explorer-overview.png) -The SQL terminal provides a `psql`-like experience in your browser. It is designed for ad-hoc usage — quick queries, debugging, and administrative tasks. - -### Connecting - - - - Go to the **Resources** tab in your PAM project, open the PostgreSQL resource, and find the account you want to access. Click the **Connect** button next to the account. +## SQL Queries - ![Account Connect Button](/images/pam/product-reference/web-access/pg-account-connect-button.png) +The SQL query runner lets you run arbitrary SQL against your database without leaving the browser. - Alternatively, if you are on the account page, click the **Access** button. +![SQL Query Runner](/images/pam/product-reference/web-access/pg-sql-query-runner.png) - ![Account Page Access Button](/images/pam/product-reference/web-access/pg-account-page-access-button.png) - - - - In the connect modal, click **Open Console** to launch the SQL terminal in a new tab. - - ![Connect Button](/images/pam/product-reference/web-access/pg-data-explorer-connect-button.png) - - - - Click the **Disconnect** button from the web access terminal status bar or type `\q`, `quit`, or `exit`. You can reconnect from the same page. - - +### Usage -![Web Access Terminal](/images/pam/product-reference/web-access/pg-web-access-terminal.png) +Click the **+** button in the tab bar to open a new query tab. Each tab has its own SQL editor and results panel. -### Usage +Write your query in the editor and run it by clicking **Run** or pressing `Cmd+Enter` (Mac) / `Ctrl+Enter` (Windows/Linux). Results are displayed in a table below the editor with column types, primary key, and foreign key indicators. -Once connected, you'll see a `=>` prompt. Type SQL queries as you would in `psql`: +You can open multiple query tabs at once. Tabs are independent: each has its own SQL content and results. -```sql -=> SELECT * FROM users LIMIT 5; - id | name | email -----+------------+--------------------- - 1 | Alice | alice@example.com - 2 | Bob | bob@example.com -(2 rows) -``` +#### Multi-statement queries -#### Transactions +If you run multiple statements at once, the results of the last statement are shown, the same behaviour as `psql`. -Transactions are fully supported. You can use `BEGIN`, `COMMIT`, and `ROLLBACK` as you would in `psql`: +#### Mutation queries -```sql -=> BEGIN; -BEGIN -=> UPDATE accounts SET balance = balance - 100 WHERE id = 1; -UPDATE 1 -=> UPDATE accounts SET balance = balance + 100 WHERE id = 2; -UPDATE 1 -=> COMMIT; -COMMIT -``` +`INSERT`, `UPDATE`, and `DELETE` statements display the number of affected rows instead of a result table. ## Limits @@ -109,10 +69,13 @@ In addition to the [common session limits](/documentation/platform/pam/product-r ## FAQ - - No. The SQL terminal is not a `psql` client. It is a custom SQL REPL that connects directly to PostgreSQL and provides a `psql`-like experience, including familiar prompts and tabular output formatting. Backslash commands like `\dt` or `\d` are not supported. - - + Editing is only available for tables that have a primary key defined. Tables without a primary key, as well as views, are displayed in read-only mode. + + Yes. You can use `BEGIN`, `COMMIT`, and `ROLLBACK` to manage transactions. An active transaction is shared across all query tabs — changes made in one tab are visible in others until you commit or roll back. The toolbar will show a **Transaction open** indicator while a transaction is in progress. + + + Yes. Select any portion of the SQL in the editor and click **Run Selection** (or press ⌘ Enter / Ctrl Enter) to execute only the selected text. + diff --git a/docs/images/pam/product-reference/web-access/pg-data-explorer-connect-button.png b/docs/images/pam/product-reference/web-access/pg-data-explorer-connect-button.png index 76cb1e54bae..deb55a27a2c 100644 Binary files a/docs/images/pam/product-reference/web-access/pg-data-explorer-connect-button.png and b/docs/images/pam/product-reference/web-access/pg-data-explorer-connect-button.png differ diff --git a/docs/images/pam/product-reference/web-access/pg-sql-query-runner.png b/docs/images/pam/product-reference/web-access/pg-sql-query-runner.png new file mode 100644 index 00000000000..a25b4968489 Binary files /dev/null and b/docs/images/pam/product-reference/web-access/pg-sql-query-runner.png differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ab852cf0192..b95751b5539 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,11 @@ "dependencies": { "@casl/ability": "^6.7.2", "@casl/react": "^4.0.0", + "@codemirror/commands": "^6.10.3", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/language": "^6.12.3", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.41.0", "@dagrejs/dagre": "^1.1.4", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", @@ -25,6 +30,7 @@ "@headlessui/react": "^1.7.19", "@hookform/resolvers": "^3.9.1", "@lexical/react": "^0.29.0", + "@lezer/highlight": "^1.2.3", "@lottiefiles/dotlottie-react": "^0.12.0", "@lottiefiles/dotlottie-web": "^0.38.2", "@octokit/rest": "^21.0.2", @@ -525,6 +531,79 @@ "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", + "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.0.tgz", + "integrity": "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@dagrejs/dagre": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz", @@ -2006,6 +2085,30 @@ "yjs": ">=13.5.22" } }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@lottiefiles/dotlottie-react": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.12.0.tgz", @@ -2024,6 +2127,12 @@ "integrity": "sha512-01d+UjJ8NG7ZStYQxtb8FPzknzGmauG7gEkcH+wHfSdiSQJY9PoBNVSTB9V6F5hAnmFqOxaocTtd7TIEEnzMnA==", "license": "MIT" }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@mdx-js/react": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", @@ -9049,6 +9158,12 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-fetch": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", @@ -16141,6 +16256,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/style-to-object": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", @@ -17163,6 +17284,12 @@ "node": ">=0.10.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2f770fdf7f4..fc3a5db9387 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,11 @@ "dependencies": { "@casl/ability": "^6.7.2", "@casl/react": "^4.0.0", + "@codemirror/commands": "^6.10.3", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/language": "^6.12.3", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.41.0", "@dagrejs/dagre": "^1.1.4", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", @@ -37,6 +42,7 @@ "@headlessui/react": "^1.7.19", "@hookform/resolvers": "^3.9.1", "@lexical/react": "^0.29.0", + "@lezer/highlight": "^1.2.3", "@lottiefiles/dotlottie-react": "^0.12.0", "@lottiefiles/dotlottie-web": "^0.38.2", "@octokit/rest": "^21.0.2", diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 8892e7d070d..062ca67c25c 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -394,10 +394,7 @@ export const ROUTE_PATHS = Object.freeze({ "/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/access", "/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/access" ), - PamDataExplorerPage: setRoute( - "/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer", - "/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer" - ), + DiscoveryPage: setRoute( "/organizations/$orgId/projects/pam/$projectId/discovery", "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/discovery/" diff --git a/frontend/src/pages/pam/PamAccountAccessPage/PamAccountAccessPage.tsx b/frontend/src/pages/pam/PamAccountAccessPage/PamAccountAccessPage.tsx index 128a2522c45..6e6d9e635c1 100644 --- a/frontend/src/pages/pam/PamAccountAccessPage/PamAccountAccessPage.tsx +++ b/frontend/src/pages/pam/PamAccountAccessPage/PamAccountAccessPage.tsx @@ -2,31 +2,27 @@ import { useState } from "react"; import { Helmet } from "react-helmet"; import { useParams } from "@tanstack/react-router"; -import { useGetPamAccountById } from "@app/hooks/api/pam"; +import { PamResourceType, useGetPamAccountById } from "@app/hooks/api/pam"; +import { PamDataExplorerPage } from "@app/pages/pam/PamDataExplorerPage/PamDataExplorerPage"; import { useWebAccessSession } from "./useWebAccessSession"; -const PageContent = () => { - const params = useParams({ - strict: false - }) as { - accountId?: string; - projectId?: string; - orgId?: string; - resourceType?: string; - resourceId?: string; - }; - - const { accountId, projectId, orgId } = params; - +const TerminalContent = ({ + accountId, + projectId, + orgId +}: { + accountId: string; + projectId: string; + orgId: string; +}) => { const { data: account, isPending } = useGetPamAccountById(accountId); - const [sessionEnded, setSessionEnded] = useState(false); const { containerRef, isConnected, disconnect, reconnect } = useWebAccessSession({ - accountId: accountId!, - projectId: projectId!, - orgId: orgId!, + accountId, + projectId, + orgId, resourceName: account?.resource.name ?? "", accountName: account?.name ?? "", resourceType: account?.resource.resourceType ?? "", @@ -111,6 +107,35 @@ const PageContent = () => { ); }; +const PageContent = () => { + const params = useParams({ + strict: false + }) as { + accountId?: string; + projectId?: string; + orgId?: string; + resourceType?: string; + resourceId?: string; + }; + + const { accountId, projectId, orgId } = params; + const { data: account, isPending } = useGetPamAccountById(accountId); + + if (isPending) { + return ( +
+ Loading... +
+ ); + } + + if (account?.resource.resourceType === PamResourceType.Postgres) { + return ; + } + + return ; +}; + export const PamAccountAccessPage = () => { return ( <> diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx index 14ef7a4a83c..84783c51758 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from "react"; import { faCopy } from "@fortawesome/free-regular-svg-icons"; -import { faTable, faTerminal, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { faTerminal, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link } from "@tanstack/react-router"; import ms from "ms"; @@ -93,8 +93,6 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account, projectId account.resource.resourceType === PamResourceType.SSH || account.resource.resourceType === PamResourceType.Redis; - const showDataExplorer = account.resource.resourceType === PamResourceType.Postgres; - return ( Browser

Connect directly from your browser

- {showDataExplorer && ( - - - Open Data Explorer - - )} - {showDataExplorer ? "Open Console" : "Connect in Browser"} + Connect in Browser
diff --git a/frontend/src/pages/pam/PamDataExplorerPage/PamDataExplorerPage.tsx b/frontend/src/pages/pam/PamDataExplorerPage/PamDataExplorerPage.tsx index 682c2e420ea..789ef9cb000 100644 --- a/frontend/src/pages/pam/PamDataExplorerPage/PamDataExplorerPage.tsx +++ b/frontend/src/pages/pam/PamDataExplorerPage/PamDataExplorerPage.tsx @@ -1,6 +1,15 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useParams } from "@tanstack/react-router"; -import { AlertTriangleIcon, DatabaseIcon, ShieldCheckIcon, UnplugIcon } from "lucide-react"; +import { + AlertTriangleIcon, + DatabaseIcon, + PlusIcon, + ShieldCheckIcon, + TableIcon, + TerminalSquareIcon, + UnplugIcon, + XIcon +} from "lucide-react"; import { Spinner } from "@app/components/v2"; import { @@ -14,12 +23,15 @@ import { AlertDialogTitle } from "@app/components/v3/generic/AlertDialog"; import { Button } from "@app/components/v3/generic/Button"; +import { cn } from "@app/components/v3/utils"; import { useGetPamAccountById } from "@app/hooks/api/pam"; import { DataExplorerGrid } from "./components/DataExplorerGrid"; import { DataExplorerSidebar } from "./components/DataExplorerSidebar"; +import { QueryPanel } from "./components/QueryPanel"; import type { SchemaInfo, TableDetail, TableInfo } from "./data-explorer-types"; import { useDataExplorerSession } from "./use-data-explorer-session"; +import { BROWSE_TAB_ID, useQueryTabs } from "./use-query-tabs"; export const PamDataExplorerPage = () => { const { accountId, projectId, orgId } = useParams({ @@ -49,6 +61,10 @@ export const PamDataExplorerPage = () => { const [approvalJustification, setApprovalJustification] = useState(""); + const { tabs, activeTabId, atTabLimit, addTab, closeTab, setActiveTab, updateTabSql } = + useQueryTabs(); + const [isInTransaction, setIsInTransaction] = useState(false); + const { isConnected, isConnecting, @@ -64,7 +80,8 @@ export const PamDataExplorerPage = () => { fetchSchemas, fetchTables, fetchTableDetail, - executeQuery + executeQuery, + cancelQuery } = useDataExplorerSession({ accountId, projectId, @@ -201,15 +218,19 @@ export const PamDataExplorerPage = () => { const handleTableSelect = useCallback( (tableName: string) => { - if (tableName === selectedTable) return; + if (tableName === selectedTable) { + setActiveTab(BROWSE_TAB_ID); + return; + } if (unsavedChangeCountRef.current > 0) { setPendingTableSwitch(tableName); return; } setSelectedTable(tableName); loadTableDetail(selectedSchema, tableName); + setActiveTab(BROWSE_TAB_ID); }, - [selectedTable, selectedSchema, loadTableDetail] + [selectedTable, selectedSchema, loadTableDetail, setActiveTab] ); const handleDiscardAndSwitch = useCallback(() => { @@ -218,11 +239,8 @@ export const PamDataExplorerPage = () => { setSelectedTable(pendingTableSwitch); loadTableDetail(selectedSchema, pendingTableSwitch); setPendingTableSwitch(null); - }, [pendingTableSwitch, selectedSchema, loadTableDetail]); - - const handleChangeCountUpdate = useCallback((count: number) => { - unsavedChangeCountRef.current = count; - }, []); + setActiveTab(BROWSE_TAB_ID); + }, [pendingTableSwitch, selectedSchema, loadTableDetail, setActiveTab]); const handleFullRefresh = useCallback(async () => { // 1. Re-fetch tables for current schema (picks up new/dropped tables) @@ -390,28 +408,111 @@ export const PamDataExplorerPage = () => { isLoadingTables={isLoadingTables} /> - {selectedTable ? ( - t.name === selectedTable)?.tableType} - schema={selectedSchema} - table={selectedTable} - executeQuery={executeQuery} - isLoading={isLoadingDetail} - onChangeCountUpdate={handleChangeCountUpdate} - onFullRefresh={handleFullRefresh} - /> - ) : ( -
-
- -

- Select a table from the sidebar to browse data -

-
+
+
+ + + {tabs.map((tab) => ( +
setActiveTab(tab.id)} + onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && setActiveTab(tab.id)} + className={cn( + "group flex shrink-0 cursor-pointer items-center gap-1 border-b-2 px-3 py-2 text-xs font-medium transition-colors", + activeTabId === tab.id + ? "border-info text-mineshaft-100" + : "border-transparent text-mineshaft-400 hover:text-mineshaft-200" + )} + > + + + {tab.title} + + +
+ ))} + +
- )} + +
+ {selectedTable ? ( + t.name === selectedTable)?.tableType} + schema={selectedSchema} + table={selectedTable} + executeQuery={executeQuery} + isLoading={isLoadingDetail} + onChangeCountUpdate={(count) => { + unsavedChangeCountRef.current = count; + }} + onFullRefresh={handleFullRefresh} + /> + ) : ( +
+
+ +

+ Select a table from the sidebar to browse data +

+
+
+ )} +
+ + {tabs.map((tab) => ( +
+ updateTabSql(tab.id, sql)} + onTransactionStateChange={setIsInTransaction} + /> +
+ ))} +
{/* Status bar */} diff --git a/frontend/src/pages/pam/PamDataExplorerPage/components/DataExplorerGrid.tsx b/frontend/src/pages/pam/PamDataExplorerPage/components/DataExplorerGrid.tsx index a6f9b8bfb53..68beb9d70a1 100644 --- a/frontend/src/pages/pam/PamDataExplorerPage/components/DataExplorerGrid.tsx +++ b/frontend/src/pages/pam/PamDataExplorerPage/components/DataExplorerGrid.tsx @@ -11,6 +11,7 @@ import { DataGrid, useDataGrid } from "@app/components/v3/generic/DataGrid"; import { Skeleton } from "@app/components/v3/generic/Skeleton"; import type { ColumnInfo, FieldInfo, ForeignKeyInfo, TableDetail } from "../data-explorer-types"; +import { getColumnIndicator } from "../data-explorer-utils"; import type { FilterCondition, SortCondition } from "../sql-generation"; import { buildCountQuery, @@ -117,23 +118,6 @@ const SELECT_COLUMN: ColumnDef = { enableResizing: false }; -function getColumnIndicator( - colName: string, - primaryKeys: string[], - fkMap: Map -): { type: "pk" | "fk"; tooltip?: string } | undefined { - if (primaryKeys.includes(colName)) return { type: "pk" }; - const fk = fkMap.get(colName); - if (fk) { - const targetCol = fk.targetColumns[fk.columns.indexOf(colName)] ?? fk.targetColumns[0]; - return { - type: "fk", - tooltip: `\u2192 ${fk.targetSchema}.${fk.targetTable}(${targetCol})` - }; - } - return undefined; -} - function buildColumnDefs( cols: ColumnInfo[], primaryKeys: string[], diff --git a/frontend/src/pages/pam/PamDataExplorerPage/components/QueryPanel.tsx b/frontend/src/pages/pam/PamDataExplorerPage/components/QueryPanel.tsx new file mode 100644 index 00000000000..ab4204717e3 --- /dev/null +++ b/frontend/src/pages/pam/PamDataExplorerPage/components/QueryPanel.tsx @@ -0,0 +1,188 @@ +import { useEffect, useRef, useState } from "react"; +import { GripHorizontalIcon } from "lucide-react"; + +import { cn } from "@app/components/v3/utils"; + +import type { FieldInfo } from "../data-explorer-types"; +import type { QueryTab } from "../use-query-tabs"; +import { QueryResultsTable } from "./QueryResultsTable"; +import { QueryToolbar } from "./QueryToolbar"; +import { SqlEditor } from "./SqlEditor"; + +function getRowLabel(rowCount: number, isTruncated: boolean): string { + if (isTruncated) return `Showing 1,000 of ${rowCount.toLocaleString()} rows`; + return `${rowCount} row${rowCount !== 1 ? "s" : ""}`; +} + +type QueryResult = { + rows: Record[]; + fields: FieldInfo[]; + rowCount: number | null; + isTruncated: boolean; + transactionOpen: boolean; + command: string; + executionTimeMs: number; +}; + +type Props = { + tab: QueryTab; + executeQuery: (sql: string) => Promise; + cancelQuery: () => void; + isInTransaction: boolean; + onSqlChange: (sql: string) => void; + onTransactionStateChange: (open: boolean) => void; +}; + +export function QueryPanel({ + tab, + executeQuery, + cancelQuery, + isInTransaction, + onSqlChange, + onTransactionStateChange +}: Props) { + const [isRunning, setIsRunning] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [hasSelection, setHasSelection] = useState(false); + const sqlToRunRef = useRef(tab.sql); + + const containerRef = useRef(null); + const editorPaneRef = useRef(null); + const splitPctRef = useRef(40); + + useEffect(() => { + if (editorPaneRef.current) { + editorPaneRef.current.style.height = `${splitPctRef.current}%`; + } + }, []); + + const runSql = async (sqlToRun: string) => { + setIsRunning(true); + setError(null); + try { + const res = await executeQuery(sqlToRun); + onTransactionStateChange(res.transactionOpen); + setResult(res); + } catch (err) { + onTransactionStateChange(false); + setError(err instanceof Error ? err.message : String(err)); + setResult(null); + } finally { + setIsRunning(false); + } + }; + + const handleRun = async (sqlToRun?: string) => { + const query = sqlToRun ?? sqlToRunRef.current; + if (!query.trim() || isRunning) return; + await runSql(query); + }; + + const handleCommit = async () => { + await runSql("COMMIT"); + }; + + const handleRollback = async () => { + await runSql("ROLLBACK"); + }; + + useEffect(() => { + if (!isDragging) return undefined; + + const onMouseMove = (e: MouseEvent) => { + const container = containerRef.current; + const editorPane = editorPaneRef.current; + if (!container || !editorPane) return; + const rect = container.getBoundingClientRect(); + const pct = Math.min(85, Math.max(10, ((e.clientY - rect.top) / rect.height) * 100)); + splitPctRef.current = pct; + editorPane.style.height = `${pct}%`; + }; + + const onMouseUp = () => setIsDragging(false); + + window.addEventListener("mousemove", onMouseMove); + window.addEventListener("mouseup", onMouseUp); + return () => { + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseup", onMouseUp); + }; + }, [isDragging]); + + return ( +
+ +
+
+ handleRun(s)} + onSelectionChange={setHasSelection} + onSqlToRunChange={(s) => { + sqlToRunRef.current = s; + }} + /> +
+ + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
{ + e.preventDefault(); + setIsDragging(true); + }} + > +
+
+ +
+
+ +
+
+ +
+ {!isRunning && result && !error && ( +
+ + {result.rowCount != null + ? getRowLabel(result.rowCount, result.isTruncated) + : result.command} + {" · "} + {result.executionTimeMs}ms + +
+ )} +
+
+
+ ); +} diff --git a/frontend/src/pages/pam/PamDataExplorerPage/components/QueryResultsTable.tsx b/frontend/src/pages/pam/PamDataExplorerPage/components/QueryResultsTable.tsx new file mode 100644 index 00000000000..6b7c29e0631 --- /dev/null +++ b/frontend/src/pages/pam/PamDataExplorerPage/components/QueryResultsTable.tsx @@ -0,0 +1,134 @@ +import { useCallback, useMemo } from "react"; +import type { ColumnDef } from "@tanstack/react-table"; + +import { Spinner } from "@app/components/v2"; +import { DataGrid, useDataGrid } from "@app/components/v3/generic/DataGrid"; + +import type { FieldInfo } from "../data-explorer-types"; + +type QueryResult = { + rows: Record[]; + fields: FieldInfo[]; + rowCount: number | null; + isTruncated: boolean; + command: string; + executionTimeMs: number; +}; + +type Props = { + result: QueryResult | null; + error: string | null; + isRunning: boolean; +}; + +type RowData = Record; + +function buildColumns(fields: FieldInfo[]): ColumnDef[] { + return fields.map((f) => ({ + id: f.name, + accessorKey: f.name, + header: f.name, + meta: { + label: f.name, + cell: { variant: "short-text" as const } + }, + enableSorting: true, + enablePinning: true, + enableHiding: true + })); +} + +function ResultsGrid({ result }: { result: QueryResult }) { + const { isTruncated } = result; + + const columns = useMemo(() => buildColumns(result.fields), [result.fields]); + + const getRowId = useCallback((_row: RowData, index: number) => String(index), []); + + const gridProps = useDataGrid({ + data: result.rows, + columns, + getRowId, + readOnly: true, + rowHeight: "short", + enableSearch: true + }); + + return ( +
+
+ +
+ {isTruncated && ( +
+ Showing first {result.rows.length.toLocaleString()} rows (results truncated) +
+ )} +
+ ); +} + +export function QueryResultsTable({ result, error, isRunning }: Props) { + if (isRunning) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+
+ {error} +
+
+ ); + } + + if (!result) { + return ( +
+

Run a query to see results

+
+ ); + } + + if (result.rows.length === 0) { + const cmdUpper = result.command.toUpperCase(); + const count = result.rowCount ?? 0; + const verb: Record = { + INSERT: "inserted", + UPDATE: "updated", + DELETE: "deleted" + }; + const staticMessage: Record = { + BEGIN: "Transaction started", + COMMIT: "Transaction committed", + ROLLBACK: "Transaction rolled back" + }; + + const isMutation = verb[cmdUpper] !== undefined && result.fields.length === 0; + + let message: string; + if (staticMessage[cmdUpper]) { + message = staticMessage[cmdUpper]; + } else if (isMutation) { + message = `${count} row${count !== 1 ? "s" : ""} ${verb[cmdUpper]}`; + } else { + message = "No rows returned"; + } + + return ( +
+ + {message} + {isMutation && · No rows returned} + +
+ ); + } + + return ; +} diff --git a/frontend/src/pages/pam/PamDataExplorerPage/components/QueryToolbar.tsx b/frontend/src/pages/pam/PamDataExplorerPage/components/QueryToolbar.tsx new file mode 100644 index 00000000000..edb5ae893ed --- /dev/null +++ b/frontend/src/pages/pam/PamDataExplorerPage/components/QueryToolbar.tsx @@ -0,0 +1,73 @@ +import { CheckIcon, PlayIcon, RotateCcwIcon, SquareIcon } from "lucide-react"; + +import { Button } from "@app/components/v3/generic/Button"; + +const isMac = typeof navigator !== "undefined" && navigator.userAgent.includes("Mac"); + +type Props = { + isRunning: boolean; + isInTransaction: boolean; + hasSelection: boolean; + onRun: () => void; + onCommit: () => void; + onRollback: () => void; + onCancel: () => void; +}; + +export function QueryToolbar({ + isRunning, + isInTransaction, + hasSelection, + onRun, + onCommit, + onRollback, + onCancel +}: Props) { + return ( +
+
+ {isRunning ? ( + + ) : ( + + )} + {isInTransaction && ( + <> + + + + )} +
+ {isInTransaction && ( + + Transaction open + + )} +
+ ); +} diff --git a/frontend/src/pages/pam/PamDataExplorerPage/components/SqlEditor.tsx b/frontend/src/pages/pam/PamDataExplorerPage/components/SqlEditor.tsx new file mode 100644 index 00000000000..0426094cd72 --- /dev/null +++ b/frontend/src/pages/pam/PamDataExplorerPage/components/SqlEditor.tsx @@ -0,0 +1,154 @@ +import { useEffect, useRef } from "react"; +import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; +import { PostgreSQL, sql } from "@codemirror/lang-sql"; +import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; +import { EditorState, type Transaction } from "@codemirror/state"; +import { EditorView, keymap, type ViewUpdate } from "@codemirror/view"; +import { tags } from "@lezer/highlight"; + +const infisicalTheme = EditorView.theme({ + "&": { height: "100%", fontSize: "13px", backgroundColor: "#16181a" }, + "&.cm-editor": { backgroundColor: "#16181a" }, + ".cm-scroller": { + overflow: "auto", + fontFamily: "ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, monospace", + backgroundColor: "#16181a", + scrollbarWidth: "thin", + scrollbarColor: "#39393d transparent" + }, + ".cm-scroller::-webkit-scrollbar": { width: "4px", height: "4px" }, + ".cm-scroller::-webkit-scrollbar-track": { background: "transparent" }, + ".cm-scroller::-webkit-scrollbar-thumb": { background: "#39393d", borderRadius: "2px" }, + ".cm-content": { padding: "8px 0", caretColor: "#e0ed34", backgroundColor: "#16181a" }, + ".cm-line": { backgroundColor: "transparent" }, + ".cm-gutters": { + backgroundColor: "#16181a", + borderRight: "1px solid #2b2c30", + color: "#707174" + }, + ".cm-lineNumbers .cm-gutterElement": { padding: "0 12px 0 8px" }, + ".cm-activeLine": { backgroundColor: "rgba(45, 47, 51, 0.5)" }, + ".cm-activeLineGutter": { backgroundColor: "rgba(45, 47, 51, 0.5)" }, + ".cm-cursor": { borderLeftColor: "#e0ed34" }, + ".cm-selectionBackground": { backgroundColor: "#2d2f33 !important" }, + "&.cm-focused .cm-selectionBackground": { backgroundColor: "#2d2f33 !important" }, + ".cm-matchingBracket": { backgroundColor: "#323439", color: "#e0ed34 !important" } +}); + +const infisicalHighlight = HighlightStyle.define( + [ + { tag: tags.keyword, color: "#63b0bd", fontWeight: "600" }, + { tag: tags.string, color: "#29b866" }, + { tag: tags.number, color: "#f39c12" }, + { tag: tags.comment, color: "#707174", fontStyle: "italic" }, + { tag: tags.operator, color: "#adaeb0" }, + { tag: tags.punctuation, color: "#adaeb0" }, + { tag: tags.separator, color: "#adaeb0" }, + { tag: tags.bracket, color: "#adaeb0" }, + { tag: tags.name, color: "#ebebeb" }, + { tag: tags.function(tags.name), color: "#63b0bd" }, + { tag: tags.typeName, color: "#f39c12" }, + { tag: tags.bool, color: "#63b0bd" }, + { tag: tags.null, color: "#707174" }, + { tag: tags.special(tags.string), color: "#29b866" }, + { tag: tags.invalid, color: "#e74c3c" } + ], + { all: { color: "#ebebeb" } } +); + +const MAX_SQL_BYTES = 50 * 1024; // 50KB — matches backend Zod limit + +const maxSqlLength = EditorState.transactionFilter.of((tr: Transaction) => { + if (tr.docChanged && tr.newDoc.length > MAX_SQL_BYTES) return []; + return tr; +}); + +type Props = { + value: string; + onChange: (value: string) => void; + onExecute: (sql: string) => void; + onSelectionChange: (hasSelection: boolean) => void; + onSqlToRunChange: (sql: string) => void; +}; + +export function SqlEditor({ + value, + onChange, + onExecute, + onSelectionChange, + onSqlToRunChange +}: Props) { + const containerRef = useRef(null); + const viewRef = useRef(null); + const onExecuteRef = useRef(onExecute); + const onChangeRef = useRef(onChange); + const onSelectionChangeRef = useRef(onSelectionChange); + const onSqlToRunChangeRef = useRef(onSqlToRunChange); + onExecuteRef.current = onExecute; + onChangeRef.current = onChange; + onSelectionChangeRef.current = onSelectionChange; + onSqlToRunChangeRef.current = onSqlToRunChange; + + useEffect(() => { + if (!containerRef.current) return undefined; + + const view = new EditorView({ + state: EditorState.create({ + doc: value, + extensions: [ + history(), + keymap.of([ + { + key: "Mod-Enter", + run: (v) => { + const selection = v.state.selection.main; + const selectedText = selection.empty + ? v.state.doc.toString() + : v.state.sliceDoc(selection.from, selection.to); + onExecuteRef.current(selectedText); + return true; + } + }, + ...historyKeymap, + ...defaultKeymap + ]), + maxSqlLength, + sql({ dialect: PostgreSQL }), + infisicalTheme, + syntaxHighlighting(infisicalHighlight), + EditorView.updateListener.of((update: ViewUpdate) => { + if (update.docChanged) { + const doc = update.state.doc.toString(); + onChangeRef.current(doc); + if (update.state.selection.main.empty) { + onSqlToRunChangeRef.current(doc); + } + } + if (update.selectionSet) { + const selection = update.state.selection.main; + if (selection.empty) { + onSqlToRunChangeRef.current(update.state.doc.toString()); + onSelectionChangeRef.current(false); + } else { + onSqlToRunChangeRef.current(update.state.sliceDoc(selection.from, selection.to)); + onSelectionChangeRef.current(true); + } + } + }) + ] + }), + parent: containerRef.current + }); + + viewRef.current = view; + onSqlToRunChangeRef.current(value); + + return () => { + view.destroy(); + viewRef.current = null; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return
; +} diff --git a/frontend/src/pages/pam/PamDataExplorerPage/data-explorer-types.ts b/frontend/src/pages/pam/PamDataExplorerPage/data-explorer-types.ts index 435da716ec6..6ed264e1c8f 100644 --- a/frontend/src/pages/pam/PamDataExplorerPage/data-explorer-types.ts +++ b/frontend/src/pages/pam/PamDataExplorerPage/data-explorer-types.ts @@ -16,6 +16,8 @@ export type DataExplorerServerMessage = rows: Record[]; fields: FieldInfo[]; rowCount: number | null; + isTruncated: boolean; + transactionOpen: boolean; command: string; executionTimeMs: number; } diff --git a/frontend/src/pages/pam/PamDataExplorerPage/data-explorer-utils.ts b/frontend/src/pages/pam/PamDataExplorerPage/data-explorer-utils.ts new file mode 100644 index 00000000000..a00fc9540a7 --- /dev/null +++ b/frontend/src/pages/pam/PamDataExplorerPage/data-explorer-utils.ts @@ -0,0 +1,15 @@ +import type { ForeignKeyInfo } from "./data-explorer-types"; + +export function getColumnIndicator( + colName: string, + primaryKeys: string[], + fkMap: Map +): { type: "pk" | "fk"; tooltip?: string } | undefined { + if (primaryKeys.includes(colName)) return { type: "pk" }; + const fk = fkMap.get(colName); + if (fk) { + const targetCol = fk.targetColumns[fk.columns.indexOf(colName)] ?? fk.targetColumns[0]; + return { type: "fk", tooltip: `\u2192 ${fk.targetSchema}.${fk.targetTable}(${targetCol})` }; + } + return undefined; +} diff --git a/frontend/src/pages/pam/PamDataExplorerPage/route.tsx b/frontend/src/pages/pam/PamDataExplorerPage/route.tsx deleted file mode 100644 index 67dd85a4395..00000000000 --- a/frontend/src/pages/pam/PamDataExplorerPage/route.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; - -import { PamDataExplorerPage } from "./PamDataExplorerPage"; - -export const Route = createFileRoute( - "/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer" -)({ - component: PamDataExplorerPage -}); diff --git a/frontend/src/pages/pam/PamDataExplorerPage/use-data-explorer-session.ts b/frontend/src/pages/pam/PamDataExplorerPage/use-data-explorer-session.ts index 5ec38ce200e..3c1894b3a41 100644 --- a/frontend/src/pages/pam/PamDataExplorerPage/use-data-explorer-session.ts +++ b/frontend/src/pages/pam/PamDataExplorerPage/use-data-explorer-session.ts @@ -392,6 +392,13 @@ export const useDataExplorerSession = ({ [sendRequest] ); + const cancelQuery = useCallback(() => { + const ws = wsRef.current; + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "cancel" })); + } + }, []); + const executeQuery = useCallback( async ( sql: string @@ -399,6 +406,8 @@ export const useDataExplorerSession = ({ rows: Record[]; fields: FieldInfo[]; rowCount: number | null; + isTruncated: boolean; + transactionOpen: boolean; command: string; executionTimeMs: number; }> => { @@ -410,6 +419,8 @@ export const useDataExplorerSession = ({ rows: resp.rows, fields: resp.fields, rowCount: resp.rowCount, + isTruncated: resp.isTruncated, + transactionOpen: resp.transactionOpen, command: resp.command, executionTimeMs: resp.executionTimeMs }; @@ -435,6 +446,7 @@ export const useDataExplorerSession = ({ fetchSchemas, fetchTables, fetchTableDetail, - executeQuery + executeQuery, + cancelQuery }; }; diff --git a/frontend/src/pages/pam/PamDataExplorerPage/use-query-tabs.ts b/frontend/src/pages/pam/PamDataExplorerPage/use-query-tabs.ts new file mode 100644 index 00000000000..878cb968a39 --- /dev/null +++ b/frontend/src/pages/pam/PamDataExplorerPage/use-query-tabs.ts @@ -0,0 +1,69 @@ +import { useState } from "react"; + +export type QueryTab = { + id: string; + title: string; + sql: string; +}; + +type TabState = { + tabs: QueryTab[]; + activeTabId: string; + nextTabNumber: number; +}; + +export const BROWSE_TAB_ID = "browse"; +const MAX_QUERY_TABS = 20; + +const DEFAULT_STATE: TabState = { tabs: [], activeTabId: BROWSE_TAB_ID, nextTabNumber: 1 }; + +export function useQueryTabs() { + const [state, setState] = useState(DEFAULT_STATE); + + const addTab = () => { + setState((prev) => { + if (prev.tabs.length >= MAX_QUERY_TABS) return prev; + const newTab: QueryTab = { + id: crypto.randomUUID(), + title: `Query ${prev.nextTabNumber}`, + sql: "" + }; + return { + tabs: [...prev.tabs, newTab], + activeTabId: newTab.id, + nextTabNumber: prev.nextTabNumber + 1 + }; + }); + }; + + const closeTab = (id: string) => { + setState((prev) => { + const idx = prev.tabs.findIndex((t) => t.id === id); + const newTabs = prev.tabs.filter((t) => t.id !== id); + const newActiveId = + prev.activeTabId === id + ? (newTabs[idx - 1]?.id ?? newTabs[0]?.id ?? BROWSE_TAB_ID) + : prev.activeTabId; + return { ...prev, tabs: newTabs, activeTabId: newActiveId }; + }); + }; + + const setActiveTab = (id: string) => setState((prev) => ({ ...prev, activeTabId: id })); + + const updateTabSql = (id: string, sql: string) => { + setState((prev) => ({ + ...prev, + tabs: prev.tabs.map((t) => (t.id === id ? { ...t, sql } : t)) + })); + }; + + return { + tabs: state.tabs, + activeTabId: state.activeTabId, + atTabLimit: state.tabs.length >= MAX_QUERY_TABS, + addTab, + closeTab, + setActiveTab, + updateTabSql + }; +} diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 948a8f60127..814bb0af3b7 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -247,7 +247,6 @@ import { Route as secretManagerIntegrationsAwsParameterStoreConfigurePageRouteIm import { Route as secretManagerIntegrationsAwsParameterStoreAuthorizePageRouteImport } from './pages/secret-manager/integrations/AwsParameterStoreAuthorizePage/route' import { Route as pamPamDiscoveryDetailPageRouteImport } from './pages/pam/PamDiscoveryDetailPage/route' import { Route as certManagerInstallationDetailsByIDPageRouteImport } from './pages/cert-manager/InstallationDetailsByIDPage/route' -import { Route as pamPamDataExplorerPageRouteImport } from './pages/pam/PamDataExplorerPage/route' import { Route as pamPamAccountAccessPageRouteImport } from './pages/pam/PamAccountAccessPage/route' import { Route as secretManagerIntegrationsVercelOauthCallbackPageRouteImport } from './pages/secret-manager/integrations/VercelOauthCallbackPage/route' import { Route as secretManagerSecretSyncDetailsByIDPageRouteImport } from './pages/secret-manager/SecretSyncDetailsByIDPage/route' @@ -2363,13 +2362,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationsOrgIdProjectsSecretManag } as any, ) -const pamPamDataExplorerPageRouteRoute = - pamPamDataExplorerPageRouteImport.update({ - id: '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer', - path: '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer', - getParentRoute: () => middlewaresInjectOrgDetailsRoute, - } as any) - const pamPamAccountAccessPageRouteRoute = pamPamAccountAccessPageRouteImport.update({ id: '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/access', @@ -4446,13 +4438,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof pamPamAccountAccessPageRouteImport parentRoute: typeof middlewaresInjectOrgDetailsImport } - '/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer': { - id: '/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer' - path: '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer' - fullPath: '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer' - preLoaderRoute: typeof pamPamDataExplorerPageRouteImport - parentRoute: typeof middlewaresInjectOrgDetailsImport - } '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId': { id: '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId' path: '/$commitId' @@ -5606,7 +5591,6 @@ interface middlewaresInjectOrgDetailsRouteChildren { organizationLayoutRoute: typeof organizationLayoutRouteWithChildren AuthenticateInjectOrgDetailsAdminRoute: typeof AuthenticateInjectOrgDetailsAdminRouteWithChildren pamPamAccountAccessPageRouteRoute: typeof pamPamAccountAccessPageRouteRoute - pamPamDataExplorerPageRouteRoute: typeof pamPamDataExplorerPageRouteRoute } const middlewaresInjectOrgDetailsRouteChildren: middlewaresInjectOrgDetailsRouteChildren = @@ -5615,7 +5599,6 @@ const middlewaresInjectOrgDetailsRouteChildren: middlewaresInjectOrgDetailsRoute AuthenticateInjectOrgDetailsAdminRoute: AuthenticateInjectOrgDetailsAdminRouteWithChildren, pamPamAccountAccessPageRouteRoute: pamPamAccountAccessPageRouteRoute, - pamPamDataExplorerPageRouteRoute: pamPamDataExplorerPageRouteRoute, } const middlewaresInjectOrgDetailsRouteWithChildren = @@ -6013,7 +5996,6 @@ export interface FileRoutesByFullPath { '/organizations/$orgId/projects/secret-management/$projectId/integrations/secret-syncs/$destination/$syncId': typeof secretManagerSecretSyncDetailsByIDPageRouteRoute '/organizations/$orgId/projects/secret-management/$projectId/integrations/vercel/oauth2/callback': typeof secretManagerIntegrationsVercelOauthCallbackPageRouteRoute '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/access': typeof pamPamAccountAccessPageRouteRoute - '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer': typeof pamPamDataExplorerPageRouteRoute '/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationsOrgIdProjectsSecretManagementProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRouteWithChildren '/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/': typeof secretManagerCommitDetailsPageRouteRoute '/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore': typeof secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteRoute @@ -6267,7 +6249,6 @@ export interface FileRoutesByTo { '/organizations/$orgId/projects/secret-management/$projectId/integrations/secret-syncs/$destination/$syncId': typeof secretManagerSecretSyncDetailsByIDPageRouteRoute '/organizations/$orgId/projects/secret-management/$projectId/integrations/vercel/oauth2/callback': typeof secretManagerIntegrationsVercelOauthCallbackPageRouteRoute '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/access': typeof pamPamAccountAccessPageRouteRoute - '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer': typeof pamPamDataExplorerPageRouteRoute '/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId': typeof secretManagerCommitDetailsPageRouteRoute '/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore': typeof secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteRoute '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId': typeof pamPamAccountByIDPageRouteRoute @@ -6551,7 +6532,6 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId': typeof secretManagerSecretSyncDetailsByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback': typeof secretManagerIntegrationsVercelOauthCallbackPageRouteRoute '/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/access': typeof pamPamAccountAccessPageRouteRoute - '/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer': typeof pamPamDataExplorerPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationsOrgIdProjectsSecretManagementProjectIdSecretManagerLayoutCommitsEnvironmentFolderIdCommitIdRouteWithChildren '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/': typeof secretManagerCommitDetailsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore': typeof secretManagerCommitDetailsPageComponentsRollbackPreviewTabRouteRoute @@ -6826,7 +6806,6 @@ export interface FileRouteTypes { | '/organizations/$orgId/projects/secret-management/$projectId/integrations/secret-syncs/$destination/$syncId' | '/organizations/$orgId/projects/secret-management/$projectId/integrations/vercel/oauth2/callback' | '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/access' - | '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer' | '/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId' | '/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/' | '/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore' @@ -7079,7 +7058,6 @@ export interface FileRouteTypes { | '/organizations/$orgId/projects/secret-management/$projectId/integrations/secret-syncs/$destination/$syncId' | '/organizations/$orgId/projects/secret-management/$projectId/integrations/vercel/oauth2/callback' | '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/access' - | '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer' | '/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId' | '/organizations/$orgId/projects/secret-management/$projectId/commits/$environment/$folderId/$commitId/restore' | '/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId' @@ -7361,7 +7339,6 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/secret-syncs/$destination/$syncId' | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/integrations/vercel/oauth2/callback' | '/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/access' - | '/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer' | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId' | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/' | '/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore' @@ -7481,8 +7458,7 @@ export const routeTree = rootRoute "children": [ "/_authenticate/_inject-org-details/_org-layout", "/_authenticate/_inject-org-details/admin", - "/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/access", - "/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer" + "/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/access" ] }, "/_authenticate/personal-settings": { @@ -8868,10 +8844,6 @@ export const routeTree = rootRoute "filePath": "pam/PamAccountAccessPage/route.tsx", "parent": "/_authenticate/_inject-org-details" }, - "/_authenticate/_inject-org-details/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer": { - "filePath": "pam/PamDataExplorerPage/route.tsx", - "parent": "/_authenticate/_inject-org-details" - }, "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId": { "filePath": "", "parent": "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/secret-management/$projectId/_secret-manager-layout/commits/$environment/$folderId", diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 9ef979376ec..4d75233a966 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -414,11 +414,6 @@ const pamAccessRoute = route( "pam/PamAccountAccessPage/route.tsx" ); -const pamDataExplorerRoute = route( - "/organizations/$orgId/projects/pam/$projectId/resources/$resourceType/$resourceId/accounts/$accountId/data-explorer", - "pam/PamDataExplorerPage/route.tsx" -); - const organizationRoutes = route("/organizations/$orgId", [ route("/projects", "organization/ProjectsPage/route.tsx"), route("/access-management", "organization/AccessManagementPage/route.tsx"), @@ -482,7 +477,6 @@ export const routes = rootRoute("root.tsx", [ middleware("inject-org-details.tsx", [ adminRoute, pamAccessRoute, - pamDataExplorerRoute, layout("org-layout", "organization/layout.tsx", [ organizationRoutes, route("/organizations/$orgId/secret-manager/$projectId", [