From 94723a4008c00b58a515c63e1a892e367eb30c66 Mon Sep 17 00:00:00 2001 From: Lasha Kakabadze Date: Wed, 8 Nov 2023 19:56:40 +0000 Subject: [PATCH 001/111] chore(ui): update snippet text --- .../DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx index 8e3b4986..deaf2191 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx @@ -292,7 +292,7 @@ export const AnsibleInstance = ({

{cluster ? '6. After the code snippet runs successfully, follow the directions displayed in the resulting output to start using the database.' - : '7. After the code snippet runs successfully, follow the directions displayed in the resulting output to start using DBLab AUI/API/CLI.'} + : '7. After the code snippet runs successfully, follow the directions displayed in the resulting output to start using DBLab UI/API/CLI.'}

Date: Fri, 10 Nov 2023 18:04:22 +0700 Subject: [PATCH 002/111] fix: prohibit the use of slashes in clone identifiers (#558) --- engine/internal/srv/server.go | 2 +- engine/internal/validator/validator.go | 5 +++++ engine/internal/validator/validator_test.go | 7 +++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/engine/internal/srv/server.go b/engine/internal/srv/server.go index e86d3232..04525053 100644 --- a/engine/internal/srv/server.go +++ b/engine/internal/srv/server.go @@ -187,7 +187,7 @@ func (s *Server) Reload(cfg srvCfg.Config) { // InitHandlers initializes handler functions of the HTTP server. func (s *Server) InitHandlers() { - r := mux.NewRouter().StrictSlash(true) + r := mux.NewRouter().StrictSlash(true).UseEncodedPath() authMW := mw.NewAuth(s.Config.VerificationToken, s.Platform) diff --git a/engine/internal/validator/validator.go b/engine/internal/validator/validator.go index 6e50f0ef..87656c2b 100644 --- a/engine/internal/validator/validator.go +++ b/engine/internal/validator/validator.go @@ -7,6 +7,7 @@ package validator import ( "fmt" + "strings" "github.com/pkg/errors" passwordvalidator "github.com/wagslane/go-password-validator" @@ -34,6 +35,10 @@ func (v Service) ValidateCloneRequest(cloneRequest *types.CloneCreateRequest) er return errors.New("missing DB password") } + if cloneRequest.ID != "" && strings.Contains(cloneRequest.ID, "/") { + return errors.New("Clone ID cannot contain slash ('/'). Please choose another ID") + } + if err := passwordvalidator.Validate(cloneRequest.DB.Password, minEntropyBits); err != nil { return fmt.Errorf("password validation: %w", err) } diff --git a/engine/internal/validator/validator_test.go b/engine/internal/validator/validator_test.go index 03186875..a510319e 100644 --- a/engine/internal/validator/validator_test.go +++ b/engine/internal/validator/validator_test.go @@ -55,6 +55,13 @@ func TestValidationCloneRequestErrors(t *testing.T) { createRequest: types.CloneCreateRequest{DB: &types.DatabaseRequest{Password: "password"}}, error: "missing DB username", }, + { + createRequest: types.CloneCreateRequest{ + DB: &types.DatabaseRequest{Username: "user", Password: "password"}, + ID: "test/ID", + }, + error: "Clone ID cannot contain slash ('/'). Please choose another ID", + }, } for _, tc := range testCases { From 92c1496a153f3ca70283368b7a0c7126dcb593f7 Mon Sep 17 00:00:00 2001 From: Lasha Kakabadze Date: Thu, 16 Nov 2023 15:02:44 +0000 Subject: [PATCH 003/111] fix(ui): update card container height correctly, improve cypress test scenario, fix console warnings --- assets/database-lab-dark-mode.svg | 8 ++-- assets/database-lab-light-mode.svg | 8 ++-- assets/dle-simple.svg | 8 ++-- assets/dle.svg | 12 +++--- assets/dle_button.svg | 12 +++--- ui/packages/ce/cypress/e2e/tabs.cy.js | 37 +++++++++++++++---- .../ce/src/App/Menu/Header/icons/index.tsx | 2 +- ui/packages/ce/src/App/Menu/icons/index.tsx | 2 +- ui/packages/platform/public/images/dblab.svg | 12 +++--- .../CheckupAgentFormWrapper.tsx | 2 + .../CreateClusterCards/CreateClusterCards.tsx | 10 ++++- .../CreateDbLabCards/CreateDbLabCards.tsx | 13 ++++++- .../DbLabInstances/DbLabInstances.tsx | 2 +- .../PostgresClusters/PostgresClusters.tsx | 2 +- ui/packages/shared/icons/Renewable/index.tsx | 12 +++--- .../Info/Disks/Disk/ActionsMenu/index.tsx | 4 +- ui/packages/shared/styles/icons.tsx | 16 ++++---- 17 files changed, 103 insertions(+), 59 deletions(-) diff --git a/assets/database-lab-dark-mode.svg b/assets/database-lab-dark-mode.svg index a867914c..2db3bd73 100644 --- a/assets/database-lab-dark-mode.svg +++ b/assets/database-lab-dark-mode.svg @@ -1,7 +1,7 @@ - - - - + + + + diff --git a/assets/database-lab-light-mode.svg b/assets/database-lab-light-mode.svg index 5a3c1e88..81ad331b 100644 --- a/assets/database-lab-light-mode.svg +++ b/assets/database-lab-light-mode.svg @@ -1,7 +1,7 @@ - - - - + + + + diff --git a/assets/dle-simple.svg b/assets/dle-simple.svg index be858b03..76daec73 100644 --- a/assets/dle-simple.svg +++ b/assets/dle-simple.svg @@ -1,6 +1,6 @@ - - - - + + + + diff --git a/assets/dle.svg b/assets/dle.svg index 9d056971..ab0b2f99 100644 --- a/assets/dle.svg +++ b/assets/dle.svg @@ -3,10 +3,10 @@ - - - - - - + + + + + + diff --git a/assets/dle_button.svg b/assets/dle_button.svg index 4efa2538..a03d399d 100644 --- a/assets/dle_button.svg +++ b/assets/dle_button.svg @@ -4,12 +4,12 @@ - - - - - - + + + + + + diff --git a/ui/packages/ce/cypress/e2e/tabs.cy.js b/ui/packages/ce/cypress/e2e/tabs.cy.js index 77d8e082..e60d40ba 100644 --- a/ui/packages/ce/cypress/e2e/tabs.cy.js +++ b/ui/packages/ce/cypress/e2e/tabs.cy.js @@ -1,21 +1,44 @@ /* eslint-disable no-undef */ -describe('Instance page should have "Configuration" tab with content', () => { - it('should have token in local storage', () => { +describe('Configuration tab', () => { + before(() => { + // Set the token in local storage cy.window().then((win) => { if (!win.localStorage.getItem('token')) { win.localStorage.setItem('token', 'demo-token') } }) }) - it('should have "Configuration" tab with content', () => { + + it('should have token in local storage', () => { + // Check if the token exists in local storage + cy.window() + .should('have.property', 'localStorage') + .and('have.property', 'token', 'demo-token') + }) + + it('should have "Configuration" tab with form inputs', () => { + // Visit the page cy.visit('/', { retryOnStatusCodeFailure: true, onLoad: () => { - cy.get('.MuiTabs-flexContainer').contains('Configuration') - cy.get('.MuiBox-root') - .contains('p') - .should('have.length.greaterThan', 0) + // Click on the "Configuration" tab + cy.get('.MuiTabs-flexContainer').contains('Configuration').click({ + force: true, + }) + + // Check for elements on the "Configuration" tab + cy.get('input[type="text"]').should('exist') + cy.get('input[type="checkbox"]').should('exist') + cy.get('button[type="button"]').should('exist') + + // Click on the "Cancel" button within the "Configuration" tab + cy.get('button').contains('Cancel').click({ + force: true, + }) + + // Check if the URL has changed to "/instance" + cy.url().should('eq', Cypress.config().baseUrl + '/instance') }, }) }) diff --git a/ui/packages/ce/src/App/Menu/Header/icons/index.tsx b/ui/packages/ce/src/App/Menu/Header/icons/index.tsx index b694a5cf..04efcec1 100644 --- a/ui/packages/ce/src/App/Menu/Header/icons/index.tsx +++ b/ui/packages/ce/src/App/Menu/Header/icons/index.tsx @@ -83,7 +83,7 @@ export const StarsIcon = ({ className }: { className?: string }) => ( xmlns="http://www.w3.org/2000/svg" className={className} > - + ( > diff --git a/ui/packages/platform/public/images/dblab.svg b/ui/packages/platform/public/images/dblab.svg index aea43a93..b0b5416f 100644 --- a/ui/packages/platform/public/images/dblab.svg +++ b/ui/packages/platform/public/images/dblab.svg @@ -1,10 +1,10 @@ - - - - - - + + + + + + diff --git a/ui/packages/platform/src/components/CheckupAgentForm/CheckupAgentFormWrapper.tsx b/ui/packages/platform/src/components/CheckupAgentForm/CheckupAgentFormWrapper.tsx index 87253b7e..334b13b1 100644 --- a/ui/packages/platform/src/components/CheckupAgentForm/CheckupAgentFormWrapper.tsx +++ b/ui/packages/platform/src/components/CheckupAgentForm/CheckupAgentFormWrapper.tsx @@ -38,6 +38,8 @@ export const CheckupAgentFormWrapper = (props: CheckupAgentFormProps) => { }, heading: { ...theme.typography.h3, + } as { + [key: string]: string }, fieldValue: { display: 'inline-block', diff --git a/ui/packages/platform/src/components/CreateClusterCards/CreateClusterCards.tsx b/ui/packages/platform/src/components/CreateClusterCards/CreateClusterCards.tsx index 1cf4a65b..91fcabed 100644 --- a/ui/packages/platform/src/components/CreateClusterCards/CreateClusterCards.tsx +++ b/ui/packages/platform/src/components/CreateClusterCards/CreateClusterCards.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames' import { StubContainer } from '@postgres.ai/shared/components/StubContainer' import { icons } from '@postgres.ai/shared/styles/icons' import { ConsoleButtonWrapper } from 'components/ConsoleButton/ConsoleButtonWrapper' @@ -9,9 +10,11 @@ import { messages } from '../../assets/messages' import { useStyles } from 'components/CreateDbLabCards/CreateDbLabCards' export const CreateClusterCards = ({ + isModal, props, dblabPermitted, }: { + isModal?: boolean props: DashboardProps dblabPermitted: boolean | undefined }) => { @@ -92,7 +95,12 @@ export const CreateClusterCards = ({ ] return ( - + {productData.map((product) => ( ({ + zeroMaxHeight: { + maxHeight: 0, + }, stubContainerProjects: { marginRight: '-20px', padding: '0 40px', @@ -75,9 +79,11 @@ export const useStyles = makeStyles((theme) => ({ })) export const CreatedDbLabCards = ({ + isModal, props, dblabPermitted, }: { + isModal?: boolean props: DashboardProps dblabPermitted: boolean | undefined }) => { @@ -154,7 +160,12 @@ export const CreatedDbLabCards = ({ ] return ( - + {productData.map((product) => ( - + ) diff --git a/ui/packages/platform/src/components/PostgresClusters/PostgresClusters.tsx b/ui/packages/platform/src/components/PostgresClusters/PostgresClusters.tsx index 56793c51..66a2be56 100644 --- a/ui/packages/platform/src/components/PostgresClusters/PostgresClusters.tsx +++ b/ui/packages/platform/src/components/PostgresClusters/PostgresClusters.tsx @@ -422,7 +422,7 @@ class PostgresClusters extends Component< aria-labelledby="simple-modal-title" aria-describedby="simple-modal-description" > - + ) diff --git a/ui/packages/shared/icons/Renewable/index.tsx b/ui/packages/shared/icons/Renewable/index.tsx index e16a245e..458123e1 100644 --- a/ui/packages/shared/icons/Renewable/index.tsx +++ b/ui/packages/shared/icons/Renewable/index.tsx @@ -25,42 +25,42 @@ export const RenewableIcon = React.forwardRef( > { Copy name {props.isActive && ( - <> +
{ closeMenu() @@ -92,7 +92,7 @@ export const ActionsMenu = (props: Props) => { > List snapshots - +
)} diff --git a/ui/packages/shared/styles/icons.tsx b/ui/packages/shared/styles/icons.tsx index 6661a458..c0e722dd 100644 --- a/ui/packages/shared/styles/icons.tsx +++ b/ui/packages/shared/styles/icons.tsx @@ -1795,23 +1795,23 @@ export const icons = { /> - + - + Date: Tue, 28 Nov 2023 19:44:58 +0000 Subject: [PATCH 004/111] fix: update OpenAPI spec for DBLab API 3.5.0 --- .../swagger-spec/dblab_server_swagger.yaml | 93 ++++++++++++++++--- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/engine/api/swagger-spec/dblab_server_swagger.yaml b/engine/api/swagger-spec/dblab_server_swagger.yaml index 177438c5..275470d7 100644 --- a/engine/api/swagger-spec/dblab_server_swagger.yaml +++ b/engine/api/swagger-spec/dblab_server_swagger.yaml @@ -15,7 +15,7 @@ info: license: name: AGPL v3 / Database Lab License url: https://github.com/postgres-ai/database-lab-engine/blob/master/LICENSE - version: 3.4.0 + version: 3.5.0 externalDocs: description: DBLab Docs url: https://gitlab.com/postgres-ai/docs/tree/master/docs/database-lab @@ -484,7 +484,7 @@ paths: schema: $ref: "#/components/schemas/Error" - /observation/download/{artifact_type}/{clone_id}/{session_id}: + /observation/download: get: tags: - Observation @@ -497,19 +497,19 @@ paths: schema: type: string required: true - - in: path + - in: query required: true name: "artifact_type" schema: type: "string" description: "Type of the requested artifact" - - in: path + - in: query required: true name: "clone_id" schema: type: "string" description: "Clone ID" - - in: path + - in: query required: true name: "session_id" schema: @@ -517,7 +517,7 @@ paths: description: "Session ID" responses: 200: - description: Downloaded the specified artificed of the specified + description: Downloaded the specified artifact of the specified observation session and clone 400: description: "Bad request" @@ -558,7 +558,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Instance' + $ref: "#/components/schemas/Error" example: code: "UNAUTHORIZED" message: "Check your verification token." @@ -613,7 +613,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Instance' + $ref: "#/components/schemas/Error" example: code: "UNAUTHORIZED" message: "Check your verification token." @@ -660,7 +660,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Instance' + $ref: "#/components/schemas/Error" example: code: "UNAUTHORIZED" message: "Check your verification token." @@ -693,7 +693,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Instance' + $ref: "#/components/schemas/Error" example: code: "UNAUTHORIZED" message: "Check your verification token." @@ -738,7 +738,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Instance' + $ref: "#/components/schemas/Error" example: code: "UNAUTHORIZED" message: "Check your verification token." @@ -762,6 +762,76 @@ paths: application/json: schema: $ref: "#/components/schemas/WSToken" + /admin/billing-status: + get: + tags: + - Admin + summary: Checks billing status + description: "" + operationId: billingStatus + parameters: + - in: header + name: Verification-Token + schema: + type: string + required: true + responses: + 200: + description: "Successful operation" + content: + application/json: + schema: + $ref: "#/components/schemas/BillingStatus" + 400: + description: "Bad request" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 401: + description: Unauthorized access + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + code: "UNAUTHORIZED" + message: "Check your verification token." + /admin/activate: + post: + tags: + - Admin + summary: "Activate billing" + description: "Activates billing and sends usage statistics of the instance" + operationId: activateBilling + parameters: + - in: header + name: Verification-Token + schema: + type: string + required: true + responses: + 200: + description: "Successful operation" + content: + application/json: + schema: + $ref: "#/components/schemas/Engine" + 400: + description: "Bad request" + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + 401: + description: Unauthorized access + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + code: "UNAUTHORIZED" + message: "Check your verification token." components: schemas: @@ -1245,7 +1315,6 @@ components: type: "string" dbVersion: type: "integer" - required: false tuningParams: type: "object" additionalProperties: From 46eb0030828a6cf97a7fee9015a093bdd21beacc Mon Sep 17 00:00:00 2001 From: Nikolay Samokhvalov Date: Fri, 29 Dec 2023 21:51:36 +0000 Subject: [PATCH 005/111] =?UTF-8?q?Rename:=20Database=20Lab=20Platform=20?= =?UTF-8?q?=CE=B2=20->=20Postges.ai=20Console?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/packages/platform/src/components/IndexPage/IndexPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index afdc4e66..a4801427 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -1086,7 +1086,7 @@ class IndexPage extends Component { const appBarLogo = ( - Database Lab Platform β + Postgres.ai Console ) From f42092cf46473b6dc8bf5234e6b1eb45b23de8b4 Mon Sep 17 00:00:00 2001 From: Nikolay Samokhvalov Date: Fri, 29 Dec 2023 22:01:00 +0000 Subject: [PATCH 006/111] Quickfix for CI config --- ui/.gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/.gitlab-ci.yml b/ui/.gitlab-ci.yml index 06560ad5..b101c8a6 100644 --- a/ui/.gitlab-ci.yml +++ b/ui/.gitlab-ci.yml @@ -65,8 +65,8 @@ e2e-ce-ui-test: variables: CYPRESS_CACHE_FOLDER: '$CI_PROJECT_DIR/cache/Cypress' before_script: - - apt update && apt install curl - - apt install -y libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb + - apt update + - apt install -y curl libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb - npm install -g wait-on - npm install -g pnpm # TODO: Set up caching. From f1246b0fa436cc93c7ea12954b23c8d770fb3511 Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Fri, 26 Jan 2024 18:27:12 +0000 Subject: [PATCH 007/111] Platform (UI): Update tag (v1.0) for dle-se-ansible image, adjust e2e tests --- ui/packages/ce/cypress/e2e/tabs.cy.js | 86 +++++++++++-------- .../DbLabInstanceForm/utils/index.tsx | 2 +- .../types/api/entities/instanceState.ts | 2 +- 3 files changed, 53 insertions(+), 37 deletions(-) diff --git a/ui/packages/ce/cypress/e2e/tabs.cy.js b/ui/packages/ce/cypress/e2e/tabs.cy.js index e60d40ba..8b8f40e1 100644 --- a/ui/packages/ce/cypress/e2e/tabs.cy.js +++ b/ui/packages/ce/cypress/e2e/tabs.cy.js @@ -1,45 +1,61 @@ /* eslint-disable no-undef */ -describe('Configuration tab', () => { - before(() => { - // Set the token in local storage - cy.window().then((win) => { - if (!win.localStorage.getItem('token')) { - win.localStorage.setItem('token', 'demo-token') - } - }) +Cypress.on('uncaught:exception', () => { + return false +}) + +// Function to set up intercepts for the requests +function setupIntercepts() { + cy.intercept('GET', '/healthz*', { + statusCode: 200, + body: { + edition: 'standard', + }, }) - it('should have token in local storage', () => { - // Check if the token exists in local storage - cy.window() - .should('have.property', 'localStorage') - .and('have.property', 'token', 'demo-token') + cy.intercept('GET', '/instance/retrieval*', { + statusCode: 200, + body: { + status: 'inactive', + }, }) - it('should have "Configuration" tab with form inputs', () => { - // Visit the page - cy.visit('/', { - retryOnStatusCodeFailure: true, - onLoad: () => { - // Click on the "Configuration" tab - cy.get('.MuiTabs-flexContainer').contains('Configuration').click({ - force: true, - }) - - // Check for elements on the "Configuration" tab - cy.get('input[type="text"]').should('exist') - cy.get('input[type="checkbox"]').should('exist') - cy.get('button[type="button"]').should('exist') - - // Click on the "Cancel" button within the "Configuration" tab - cy.get('button').contains('Cancel').click({ - force: true, - }) - - // Check if the URL has changed to "/instance" - cy.url().should('eq', Cypress.config().baseUrl + '/instance') + cy.intercept('GET', '/status*', { + statusCode: 200, + body: { + status: { + code: 'OK', + message: 'Instance is ready', + }, + pools: [], + cloning: { + clones: [], }, + retrieving: { + status: 'inactive', + }, + }, + }) +} + +describe('Configuration tab', () => { + beforeEach(() => { + cy.visit('/') + setupIntercepts() + }) + + it('should have "Configuration" tab with form inputs', () => { + cy.get('.MuiTabs-flexContainer').contains('Configuration').click({ + force: true, + }) + + cy.get('input[type="text"]').should('exist') + cy.get('input[type="checkbox"]').should('exist') + cy.get('button[type="button"]').should('exist') + + cy.get('button').contains('Cancel').click({ + force: true, }) + cy.url().should('eq', Cypress.config().baseUrl + '/instance') }) }) diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx index b828997a..5bf6bdd6 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx @@ -7,7 +7,7 @@ export const DEBUG_API_SERVER = 'https://v2.postgres.ai/api/general' export const availableTags = ['3.5.0', '3.4.0', '4.0.0-alpha.6'] -export const sePackageTag = 'v1.0-rc.10' +export const sePackageTag = 'v1.0' export const dockerRunCommand = (provider: string) => { /* eslint-disable no-template-curly-in-string */ diff --git a/ui/packages/shared/types/api/entities/instanceState.ts b/ui/packages/shared/types/api/entities/instanceState.ts index dc3d66e6..6fef7d54 100644 --- a/ui/packages/shared/types/api/entities/instanceState.ts +++ b/ui/packages/shared/types/api/entities/instanceState.ts @@ -58,7 +58,7 @@ export const formatInstanceStateDto = (dto: InstanceStateDto) => { const pools = dto.pools?.map(formatPoolDto) ?? null const clones = - dto?.clones?.map(formatCloneDto) ?? dto.cloning?.clones.map(formatCloneDto) + dto?.clones?.map(formatCloneDto) ?? dto.cloning?.clones?.map(formatCloneDto) const expectedCloningTime = dto?.expectedCloningTime ?? dto.cloning?.expectedCloningTime From 1b0a5ea4d5d91398573a4d3ae9f1be2c67678ddc Mon Sep 17 00:00:00 2001 From: Artyom Kartasov Date: Mon, 29 Jan 2024 09:20:15 +0000 Subject: [PATCH 008/111] chore: update URLs of the updated demo instance --- README.md | 2 +- engine/api/swagger-spec/dblab_server_swagger.yaml | 2 +- ui/packages/ce/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 73b8d499..b8273bbd 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Try it yourself right now: - Visit [Postgres.ai Console](https://console.postgres.ai/), set up your first organization and provision a DBLab Standard Edition (DBLab SE) to any cloud or on-prem - [Pricing](https://postgres.ai/pricing) (starting at $62/month) - [Doc: How to install DBLab SE](https://postgres.ai/docs/how-to-guides/administration/install-dle-from-postgres-ai) -- Demo: https://demo.aws.postgres.ai:446/instance (use the token `demo_token` to access) +- Demo: https://demo.aws.postgres.ai (use the token `demo-token` to access) - Looking for a free version? Install DBLab Community Edition by [following this tutorial](https://postgres.ai/docs/tutorials/database-lab-tutorial) ## How it works diff --git a/engine/api/swagger-spec/dblab_server_swagger.yaml b/engine/api/swagger-spec/dblab_server_swagger.yaml index 275470d7..2ca26850 100644 --- a/engine/api/swagger-spec/dblab_server_swagger.yaml +++ b/engine/api/swagger-spec/dblab_server_swagger.yaml @@ -21,7 +21,7 @@ externalDocs: url: https://gitlab.com/postgres-ai/docs/tree/master/docs/database-lab servers: - - url: "https://demo.aws.postgres.ai:446/api" + - url: "https://demo.aws.postgres.ai:446" description: "DBLab 3.x demo server; token: 'demo-token'" x-examples: Verification-Token: "demo-token" diff --git a/ui/packages/ce/package.json b/ui/packages/ce/package.json index b1d4d19c..232deab9 100644 --- a/ui/packages/ce/package.json +++ b/ui/packages/ce/package.json @@ -90,5 +90,5 @@ "stylelint-config-standard-scss": "^2.0.1", "stylelint-prettier": "^2.0.0" }, - "proxy": "https://demo.aws.postgres.ai:446/api" + "proxy": "https://demo.aws.postgres.ai:446" } From 13f600b7c8b67203440da1bed8031e661652295a Mon Sep 17 00:00:00 2001 From: Artyom Kartasov Date: Wed, 31 Jan 2024 05:25:25 +0000 Subject: [PATCH 009/111] chore: remove the recommendation section from Joe History in Platform UI --- ui/packages/platform/src/pages/JoeSessionCommand/index.js | 4 ---- ui/packages/platform/src/stores/store.js | 6 ------ 2 files changed, 10 deletions(-) diff --git a/ui/packages/platform/src/pages/JoeSessionCommand/index.js b/ui/packages/platform/src/pages/JoeSessionCommand/index.js index 30857fda..d416cc97 100644 --- a/ui/packages/platform/src/pages/JoeSessionCommand/index.js +++ b/ui/packages/platform/src/pages/JoeSessionCommand/index.js @@ -263,7 +263,6 @@ class JoeSessionCommand extends Component { const openVizDialog = showFlameGraph || (externalVisualization && externalVisualization.url && externalVisualization.url.length > 0); const title = `Command #${commandId} (${data.command}) from session #${sessionId}`; - const recommends = data.recommends; let shareUrlButton = ( + + + + +
+ + ) + } +} + +export default BotSettingsForm diff --git a/ui/packages/platform/src/components/BotSettingsForm/BotSettingsFormWrapper.tsx b/ui/packages/platform/src/components/BotSettingsForm/BotSettingsFormWrapper.tsx new file mode 100644 index 00000000..b7860227 --- /dev/null +++ b/ui/packages/platform/src/components/BotSettingsForm/BotSettingsFormWrapper.tsx @@ -0,0 +1,58 @@ +import { makeStyles } from '@material-ui/core' +import { styles } from '@postgres.ai/shared/styles/styles' +import OrgForm from 'components/OrgForm/OrgForm' +import BotSettingsForm from "./BotSettingsForm"; + +export interface BotSettingsFormProps { + mode?: string | undefined + project?: string | undefined + org?: string | number + orgId?: number + orgPermissions?: { + settingsOrganizationUpdate?: boolean + } +} + +const useStyles = makeStyles( + { + container: { + ...(styles.root as Object), + display: 'flex', + 'flex-wrap': 'wrap', + 'min-height': 0, + '&:not(:first-child)': { + 'margin-top': '20px', + }, + }, + textField: { + ...styles.inputField, + maxWidth: 450, + }, + instructionsField: { + ...styles.inputField, + maxWidth: 450, + }, + selectField: { + maxWidth: 450, + marginTop: 20, + '& .MuiInputLabel-formControl': { + transform: 'none', + position: 'static' + } + }, + updateButtonContainer: { + marginTop: 20, + textAlign: 'left', + }, + errorMessage: { + color: 'red', + }, + }, + { index: 1 }, +) + +export const BotSettingsFormWrapper = (props: BotSettingsFormProps) => { + const classes = useStyles() + + return +} diff --git a/ui/packages/platform/src/components/ContentLayout/DemoOrgNotice/index.tsx b/ui/packages/platform/src/components/ContentLayout/DemoOrgNotice/index.tsx index 22849f65..2ada292b 100644 --- a/ui/packages/platform/src/components/ContentLayout/DemoOrgNotice/index.tsx +++ b/ui/packages/platform/src/components/ContentLayout/DemoOrgNotice/index.tsx @@ -62,15 +62,14 @@ export const DemoOrgNotice = () => { return (
- {icons.infoIconBlue} This is a Demo organization, once you’ve - explored Database Lab features: + {icons.infoIconBlue} This is a demo organization. All the data here is public.
) diff --git a/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx b/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx index 62ed8071..c781f85a 100644 --- a/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx +++ b/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx @@ -20,7 +20,7 @@ const useStyles = makeStyles( justifyContent: 'center', padding: '16px 20px', [theme.breakpoints.down('sm')]: { - padding: '16px 12px', + padding: '12px 12px', flexDirection: 'column', }, }, @@ -30,6 +30,14 @@ const useStyles = makeStyles( marginBottom: 10, }, }, + footerLinks: { + display: 'flex', + [theme.breakpoints.down('sm')]: { + flexDirection: 'column', + flexWrap: 'wrap', + maxHeight: '80px', + }, + }, footerItem: { marginLeft: 10, marginRight: 10, @@ -62,36 +70,38 @@ export const Footer = () => { return (
- {new Date().getFullYear()} © Postgres.ai -
-
- - Documentation - -
-
|
-
- - News - -
-
|
-
- - Terms of Service - -
-
|
-
- - Privacy Policy - + {new Date().getFullYear()} © Postgres.AI
-
|
-
- - Ask support - +
+
+ + Documentation + +
+
|
+
+ + News + +
+
|
+
+ + Terms of Service + +
+
|
+
+ + Privacy Policy + +
+
|
+
+ + Ask support + +
) diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index a4801427..c114f03a 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -70,6 +70,7 @@ import { LoginDialogWrapper } from 'components/LoginDialog/LoginDialogWrapper' import { NotificationWrapper } from 'components/Notification/NotificationWrapper' import { SharedUrlWrapper } from 'components/SharedUrl/SharedUrlWrapper' import { ShareUrlDialogWrapper } from 'components/ShareUrlDialog/ShareUrlDialogWrapper' +import { BotWrapper } from "pages/Bot/BotWrapper"; import Actions from '../../actions/actions' import JoeConfig from '../JoeConfig' @@ -82,6 +83,9 @@ import { IndexPageProps } from 'components/IndexPage/IndexPageWrapper' import { PostgresClusterWrapper } from 'components/PostgresClusterForm/PostgresClusterWrapper' import { PostgresClusterInstallWrapper } from 'components/PostgresClusterInstallForm/PostgresClusterInstallWrapper' import { PostgresClustersWrapper } from 'components/PostgresClusters/PostgresClustersWrapper' +import cn from "classnames"; +import { BotSettingsFormWrapper } from "../BotSettingsForm/BotSettingsFormWrapper"; + interface IndexPageWithStylesProps extends IndexPageProps { classes: ClassesType @@ -229,6 +233,10 @@ function ProjectWrapper(parentProps: Omit) { path="/:org/:project/joe-instances/:instanceId" render={(props) => } /> + } + /> } @@ -344,6 +352,23 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { + {/* + + + {icons.aiBotIcon} + + AI BotNEW + + */} - Ask JoeBOT + Ask Joe )} + {/* + + AI Bot + + */} ( - + )} /> )} /> + ( + + )} + /> + ( + + )} + /> ( @@ -835,6 +892,12 @@ function OrganizationWrapper(parentProps: OrganizationWrapperProps) { )} /> + ( + + )} + /> ( @@ -1086,7 +1149,7 @@ class IndexPage extends Component { const appBarLogo = ( - Postgres.ai Console + Postgres.AI Console ) diff --git a/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx b/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx index c306a2cb..82a2742c 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx @@ -174,7 +174,7 @@ export const IndexPageWrapper = (props: IndexPageProps) => { position: 'absolute', top: '2px', }, - botTag: { + menuItemLabel: { fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', fontStyle: 'normal', fontWeight: 'normal', @@ -192,6 +192,9 @@ export const IndexPageWrapper = (props: IndexPageProps) => { position: 'absolute', top: '10px', }, + headerLinkMenuItemLabel: { + position: 'static' + }, menuSectionHeader: { fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', fontStyle: 'normal', @@ -220,6 +223,8 @@ export const IndexPageWrapper = (props: IndexPageProps) => { width: '100%', paddingLeft: '15px', color: '#000000', + display: 'inline-flex', + alignItems: 'center' }, menuSectionHeaderActiveLink: { textDecoration: 'none', @@ -230,6 +235,9 @@ export const IndexPageWrapper = (props: IndexPageProps) => { paddingLeft: '15px', color: '#000000', }, + menuSingleSectionHeaderActiveLink: { + backgroundColor: colors.consoleStroke, + }, menuPointer: { height: '100%', }, @@ -237,6 +245,9 @@ export const IndexPageWrapper = (props: IndexPageProps) => { padding: '0px', height: 'calc(100% - 160px)', overflowY: 'auto', + + display: 'flex', + flexDirection: 'column', }, menuSectionHeaderIcon: { marginRight: '13px', @@ -344,4 +355,4 @@ export const IndexPageWrapper = (props: IndexPageProps) => { const classes = useStyles() return -} +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/BotWrapper.tsx b/ui/packages/platform/src/pages/Bot/BotWrapper.tsx new file mode 100644 index 00000000..a8fe7dc0 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/BotWrapper.tsx @@ -0,0 +1,31 @@ +import { BotPage } from "./index"; +import {RouteComponentProps} from "react-router"; +import {AlertSnackbarProvider} from "@postgres.ai/shared/components/AlertSnackbar/useAlertSnackbar"; + +export interface BotWrapperProps { + envData: { + info?: { + user_name?: string + } + }; + orgData: { + id: number + }, + history: RouteComponentProps['history'] + project?: string + match: { + params: { + org?: string + threadId?: string + } + } +} + + +export const BotWrapper = (props: BotWrapperProps) => { + return ( + + + + ) +} diff --git a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx new file mode 100644 index 00000000..4bb0fa3b --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx @@ -0,0 +1,187 @@ +/*-------------------------------------------------------------------------- + * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai + * All Rights Reserved. Proprietary and confidential. + * Unauthorized copying of this file, via any medium is strictly prohibited + *-------------------------------------------------------------------------- + */ + +import React from "react"; +import { Link } from "react-router-dom"; +import { useParams } from "react-router"; +import cn from "classnames"; +import { makeStyles, Theme } from "@material-ui/core"; +import Drawer from '@material-ui/core/Drawer'; +import List from "@material-ui/core/List"; +import Divider from "@material-ui/core/Divider"; +import ListSubheader from '@material-ui/core/ListSubheader'; +import Box from "@mui/material/Box"; +import { Spinner } from "@postgres.ai/shared/components/Spinner"; +import { HeaderButtons, HeaderButtonsProps } from "../HeaderButtons/HeaderButtons"; +import { BotMessage } from "../../../types/api/entities/bot"; + + +const useStyles = makeStyles((theme) => ({ + drawerPaper: { + width: 240, + //TODO: Fix magic numbers + height: props => props.isDemoOrg ? 'calc(100vh - 122px)' : 'calc(100vh - 90px)', + marginTop: props => props.isDemoOrg ? 72 : 40, + [theme.breakpoints.down('sm')]: { + height: '100vh!important', + marginTop: '0!important', + width: 260, + zIndex: 9999 + }, + '& > ul': { + display: 'flex', + flexDirection: 'column', + '@supports (scrollbar-gutter: stable)': { + scrollbarGutter: 'stable', + paddingRight: 0, + overflow: 'hidden', + }, + '&:hover': { + overflow: 'auto' + }, + [theme.breakpoints.down('sm')]: { + paddingBottom: 120 + } + } + }, + listPadding: { + paddingTop: 0 + }, + listSubheaderRoot: { + background: 'white', + }, + listItemLink: { + fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + fontStyle: 'normal', + fontWeight: 'normal', + fontSize: '0.875rem', + lineHeight: '1rem', + color: '#000000', + width: '100%', + textOverflow: 'ellipsis', + overflow: 'hidden', + padding: '0.75rem 1rem', + whiteSpace: 'nowrap', + textDecoration: "none", + flex: '0 0 2.5rem', + '&:hover': { + background: 'rgba(0, 0, 0, 0.04)' + } + }, + listItemLinkActive: { + background: 'rgba(0, 0, 0, 0.04)' + }, + loader: { + width: '100%', + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + }) +); + +type ChatsListProps = { + isOpen: boolean; + onCreateNewChat: () => void; + onClose: () => void; + isDemoOrg: boolean; + loading: boolean; + chatsList: BotMessage[] | null; + onLinkClick?: (targetThreadId: string) => void; + permalinkId?: string +} & HeaderButtonsProps + +export const ChatsList = (props: ChatsListProps) => { + const { + isOpen, + onCreateNewChat, + onClose, + chatsList, + loading, + currentVisibility, + withChatVisibilityButton, + onChatVisibilityClick, + onLinkClick, + permalinkId + } = props; + const classes = useStyles(props); + const params = useParams<{ org?: string, threadId?: string }>(); + + const linkBuilder = (msgId: string) => { + if (params.org) { + return `/${params.org}/bot/${msgId}` + } else { + return `/bot/${msgId}` + } + } + + const handleClick = (threadId: string) => { + if (onLinkClick) { + onLinkClick(threadId) + } + } + + const loader = ( + + + + ) + + const list = ( + + + + + + + {chatsList && chatsList.map((item) => { + const isActive = item.id === params.threadId + const link = linkBuilder(item.id) + return ( + handleClick(item.id)} + > + {item.content} + + ) + }) + } + + ) + + return ( + + {loading ? loader : list} + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Command/Command.tsx b/ui/packages/platform/src/pages/Bot/Command/Command.tsx new file mode 100644 index 00000000..8d993579 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Command/Command.tsx @@ -0,0 +1,190 @@ +import React, { useState, useRef, useEffect } from 'react' +import { makeStyles } from '@material-ui/core' +import SendRoundedIcon from '@material-ui/icons/SendRounded'; +import IconButton from "@material-ui/core/IconButton"; +import { TextField } from '@postgres.ai/shared/components/TextField' +import { + checkIsSendCmd, + checkIsNewLineCmd, + addNewLine, + checkIsPrevMessageCmd, + checkIsNextMessageCmd, +} from './utils' +import { useBuffer } from './useBuffer' +import { useCaret } from './useCaret' +import { theme } from "@postgres.ai/shared/styles/theme"; + + +type Props = { + sendDisabled: boolean + onSend: (value: string) => void + threadId?: string +} + + +const useStyles = makeStyles((theme) => ( + { + root: { + display: 'flex', + alignItems: 'flex-end', + marginTop: '20px', + border: '1px solid rgba(0, 0, 0, 0.23)', + borderRadius: '8px', + '& .MuiOutlinedInput-root': { + '& fieldset': { + border: 'none', + }, + '&:hover fieldset': { + border: 'none', + }, + '&.Mui-focused fieldset': { + border: 'none', + } + } + }, + field: { + margin: '0 8px 0 0', + flex: '1 1 100%', + fontSize: '0.875rem', + }, + fieldInput: { + fontSize: '0.875rem', + lineHeight: 'normal', + height: 'auto', + padding: '12px', + [theme.breakpoints.down('sm')]: { + fontSize: '1rem' + } + }, + iconButton: { + height: 40, + width: 40, + fontSize: 24, + transition: '.2s ease', + '&:hover': { + color: '#000' + } + }, + button: { + flex: '0 0 auto', + height: '40px', + }, + }) +) + +export const Command = React.memo((props: Props) => { + const { sendDisabled, onSend, threadId } = props + + const classes = useStyles() + + // Handle value. + const [value, setValue] = useState('') + + // Input DOM Element reference. + const inputRef = useRef() + + // Messages buffer. + const buffer = useBuffer() + + // Input caret. + const caret = useCaret(inputRef) + + const triggerSend = () => { + if (!value.trim().length || sendDisabled) return + + onSend(value) + buffer.addNew() + setValue(buffer.getCurrent()) + } + + const handleChange = (e: React.ChangeEvent) => { + setValue(e.target.value) + buffer.switchToLast() + buffer.setToCurrent(e.target.value) + } + + const handleBlur = () => { + if (window.innerWidth < theme.breakpoints.values.sm) + window.scrollTo({ + top: 0, + behavior: 'smooth' + }) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!inputRef.current) return + + // Trigger to send. + if (checkIsSendCmd(e.nativeEvent)) { + e.preventDefault() + triggerSend() + return + } + + // Trigger line break. + if (checkIsNewLineCmd(e.nativeEvent)) { + e.preventDefault() + + const content = addNewLine(value, inputRef.current) + + setValue(content.value) + caret.setPosition(content.caretPosition) + return + } + + // Trigger to use prev message. + if (checkIsPrevMessageCmd(e.nativeEvent, inputRef.current)) { + e.preventDefault() + + const prevValue = buffer.switchPrev() + setValue(prevValue) + return + } + + // Trigger to use next message. + if (checkIsNextMessageCmd(e.nativeEvent, inputRef.current)) { + e.preventDefault() + + const nextValue = buffer.switchNext() + setValue(nextValue) + return + } + + // Skip other keyboard events to fill input. + } + + // Autofocus. + useEffect(() => { + if (!inputRef.current) return + if (window.innerWidth > theme.breakpoints.values.md) inputRef.current.focus() + }, [threadId]); + + + return ( +
+ + + + +
+ ) +}) diff --git a/ui/packages/platform/src/pages/Bot/Command/useBuffer.ts b/ui/packages/platform/src/pages/Bot/Command/useBuffer.ts new file mode 100644 index 00000000..0368bc44 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Command/useBuffer.ts @@ -0,0 +1,53 @@ +import { useRef } from 'react' + +type Buffer = { + values: string[] + position: number +} + +const NEW_EMPTY_VALUE = '' + +const INITIAL_BUFFER: Buffer = { + values: [NEW_EMPTY_VALUE], + position: 0, +} + +export const useBuffer = () => { + const { current: buffer } = useRef(INITIAL_BUFFER) + + const getCurrent = () => buffer.values[buffer.position] + + const setToCurrent = (value: string) => buffer.values[buffer.position] = value + + const switchNext = () => { + const newPosition = buffer.position + 1 + if (newPosition in buffer.values) buffer.position = newPosition + return getCurrent() + } + + const switchPrev = () => { + const newPosition = buffer.position - 1 + if (newPosition in buffer.values) buffer.position = newPosition + return getCurrent() + } + + const switchToLast = () => { + const lastIndex = buffer.values.length - 1 + buffer.position = lastIndex + return getCurrent() + } + + const addNew = () => { + buffer.values.push(NEW_EMPTY_VALUE) + return switchToLast() + } + + return { + switchNext, + switchPrev, + switchToLast, + addNew, + getCurrent, + setToCurrent, + } +} diff --git a/ui/packages/platform/src/pages/Bot/Command/useCaret.ts b/ui/packages/platform/src/pages/Bot/Command/useCaret.ts new file mode 100644 index 00000000..8de590a5 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Command/useCaret.ts @@ -0,0 +1,22 @@ +import { useState, useEffect, MutableRefObject } from 'react' + +export const useCaret = ( + elementRef: MutableRefObject, +) => { + // Keep caret position after making new line, but only after react update. + const [nextPosition, setNextPosition] = useState(null) + + useEffect(() => { + if (nextPosition === null) return + if (!elementRef.current) return + + elementRef.current.selectionStart = nextPosition + elementRef.current.selectionEnd = nextPosition + + setNextPosition(null) + }, [elementRef, nextPosition]) + + return { + setPosition: setNextPosition, + } +} diff --git a/ui/packages/platform/src/pages/Bot/Command/utils.ts b/ui/packages/platform/src/pages/Bot/Command/utils.ts new file mode 100644 index 00000000..47b68f46 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Command/utils.ts @@ -0,0 +1,57 @@ +export const checkIsSendCmd = (e: KeyboardEvent) => + e.code === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey + +export const checkIsNewLineCmd = (e: KeyboardEvent) => + e.code === 'Enter' && (e.shiftKey || e.ctrlKey || e.metaKey) + +export const addNewLine = ( + value: string, + element: HTMLInputElement | HTMLTextAreaElement, +) => { + const NEW_LINE_STR = '\n' + + const firstLineLength = element.selectionStart ?? value.length + const secondLineLength = element.selectionEnd ?? value.length + + const firstLine = value.substring(0, firstLineLength) + const secondLine = value.substring(secondLineLength) + + return { + value: `${firstLine}${NEW_LINE_STR}${secondLine}`, + caretPosition: firstLineLength + NEW_LINE_STR.length, + } +} + +export const checkIsPrevMessageCmd = ( + e: KeyboardEvent, + element: HTMLInputElement | HTMLTextAreaElement, +) => { + const isRightKey = + e.code === 'ArrowUp' && !e.ctrlKey && !e.metaKey && !e.shiftKey + + // Use prev message only if the caret is in the start of the input. + const targetCaretPosition = 0 + + const isRightCaretPosition = + element.selectionStart === targetCaretPosition && + element.selectionEnd === targetCaretPosition + + return isRightKey && isRightCaretPosition +} + +export const checkIsNextMessageCmd = ( + e: KeyboardEvent, + element: HTMLInputElement | HTMLTextAreaElement, +) => { + const isRightKey = + e.code === 'ArrowDown' && !e.ctrlKey && !e.metaKey && !e.shiftKey + + // Use next message only if the caret is in the end of the input. + const targetCaretPosition = element.value.length + + const isRightCaretPosition = + element.selectionStart === targetCaretPosition && + element.selectionEnd === targetCaretPosition + + return isRightKey && isRightCaretPosition +} diff --git a/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx b/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx new file mode 100644 index 00000000..0ee4ac38 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx @@ -0,0 +1,101 @@ +import React from 'react' +import Dialog from "@material-ui/core/Dialog"; +import DialogTitle from '@material-ui/core/DialogTitle'; +import { DialogContent, IconButton, makeStyles, Typography } from "@material-ui/core"; +import ReactMarkdown from "react-markdown"; +import Format from "../../../utils/format"; +import { icons } from "@postgres.ai/shared/styles/icons"; + +type DebugDialogProps = { + isOpen: boolean; + onClose: () => void; + debugMessages: { created_at: string, message: string }[] +} + +const useStyles = makeStyles( + (theme) => ({ + dialogTitle: { + fontSize: '1rem', + paddingBottom: '8px', + '& > *': { + fontSize: 'inherit' + } + }, + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, + message: { + marginBottom: 12, + '& > *': { + fontFamily: "'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New'," + + " 'andale mono', 'lucida console', monospace", + fontSize: '0.813rem', + lineHeight: '120%' + }, + }, + time: { + marginBottom: 4, + display: 'inline-block' + } + })) + +export const DebugDialog = (props: DebugDialogProps) => { + const {isOpen, onClose, debugMessages} = props; + const classes = useStyles() + return ( + + + Debug + + {icons.closeIcon} + + + + { + debugMessages.map((debugMessage) => { + const formattedTime = debugMessage.created_at ? Format.timeAgo(debugMessage.created_at) : null + return ( +
+ + {formattedTime} + + + {debugMessage.message} + +
+ ) + } + ) + } + { + (!debugMessages || debugMessages.length === 0) && ( +
+ No debug information available for the selected message. +
+ ) + } +
+
+ ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/HeaderButtons/HeaderButtons.tsx b/ui/packages/platform/src/pages/Bot/HeaderButtons/HeaderButtons.tsx new file mode 100644 index 00000000..1c4ab51d --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/HeaderButtons/HeaderButtons.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import IconButton from "@material-ui/core/IconButton"; +import NavigateBeforeIcon from "@material-ui/icons/NavigateBefore"; +import NavigateNextIcon from '@material-ui/icons/NavigateNext'; +import { Button, makeStyles, useMediaQuery } from "@material-ui/core"; +import AddCircleOutlineIcon from "@material-ui/icons/AddCircleOutline"; +import Box from "@mui/material/Box"; +import { theme } from "@postgres.ai/shared/styles/theme"; +import { SettingsWithLabel } from "../SettingsWithLabel/SettingsWithLabel"; + + +export type HeaderButtonsProps = { + isOpen: boolean; + onClose: () => void; + onCreateNewChat: () => void; + withChatVisibilityButton: boolean; + onChatVisibilityClick?: () => void; + currentVisibility: 'public' | 'private'; + permalinkId?: string; +} + +const useStyles = makeStyles((theme) => ({ + container: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '8px 5px', + flex: 1, + [theme.breakpoints.down('sm')]: { + justifyContent: 'flex-end', + } + }, + hideChatButton: { + width: '2rem', + height: '2rem' + }, + hideChatButtonIcon: { + width: '2rem', + height: '2rem', + fill: '#000' + }, + createNewChatButton: { + [theme.breakpoints.down('sm')]: { + border: 'none', + minWidth: '2rem', + height: '2rem', + padding: 0, + marginRight: '0.5rem', + '& .MuiButton-startIcon': { + margin: 0 + } + } + } +})) + +export const HeaderButtons = (props: HeaderButtonsProps) => { + const { + onClose, + onCreateNewChat, + isOpen, + onChatVisibilityClick, + withChatVisibilityButton, + currentVisibility, + permalinkId + } = props; + const matches = useMediaQuery(theme.breakpoints.down('sm')); + const classes = useStyles(); + + return ( + + { + withChatVisibilityButton && onChatVisibilityClick && + + } + + + {isOpen + ? + : + } + + + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx new file mode 100644 index 00000000..5d2289b5 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx @@ -0,0 +1,143 @@ +import React, { useState } from 'react'; +import { Accordion, AccordionDetails, AccordionSummary, Typography, makeStyles, Button } from '@material-ui/core'; +import { Prism as SyntaxHighlighter, SyntaxHighlighterProps } from 'react-syntax-highlighter' +import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism' +import languages from 'react-syntax-highlighter/dist/esm/languages/prism/supported-languages'; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined'; +import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: '.5em', + width: '100%' + }, + header: { + background: 'rgb(240, 240, 240)', + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + padding: '.2rem 1rem', + display: 'flex' + }, + languageName: { + fontSize: '0.813rem', + color: theme.palette.text.primary + }, + copyButton: { + marginLeft: 'auto', + color: theme.palette.text.primary, + padding: '0', + minHeight: 'auto', + fontSize: '0.813rem', + border: 0, + '&:hover': { + backgroundColor: 'transparent' + } + }, + copyButtonIcon: { + width: '0.813rem', + }, + summary: { + textDecoration: 'underline', + textDecorationStyle: 'dotted', + cursor: 'pointer', + backgroundColor: 'transparent', + boxShadow: 'none', + display: 'inline-flex', + minHeight: '32px!important', + padding: 0, + '&:hover': { + textDecoration: 'none' + } + }, + details: { + padding: 0, + backgroundColor: 'transparent' + }, + accordion: { + boxShadow: 'none', + backgroundColor: 'transparent', + }, + pre: { + width: '100%', + marginTop: '0!important', + + } +})); + +export const CodeBlock = ({ value, language }: { value: string, language?: string | null }) => { + const classes = useStyles(); + const [expanded, setExpanded] = useState(false); + const [copied, setCopied] = useState(false); + + const codeLines = value.split('\n'); + const handleToggle = () => setExpanded(!expanded); + + + const isValidLanguage = language && languages.includes(language); + + const syntaxHighlighterProps: SyntaxHighlighterProps = { + showLineNumbers: true, + language: language || 'sql', + style: oneLight, + className: classes.pre, + children: value + } + + const handleCopy = () => { + if ('clipboard' in navigator) { + navigator.clipboard.writeText(value).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + } + }; + + + const header = ( +
+ {isValidLanguage && {language}} + +
+ ) + + if (codeLines.length > 20) { + return ( + + } className={classes.summary}> + {expanded ? 'Hide' : 'Show'} code block ({codeLines.length} LOC) + + +
+ {header} + +
+
+
+ ); + } + + return ( +
+ {header} + +
+ ); +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx new file mode 100644 index 00000000..a2e59623 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx @@ -0,0 +1,328 @@ +import React, { useState } from 'react' +import cn from "classnames"; +import ReactMarkdown from "react-markdown"; +import rehypeRaw from "rehype-raw"; +import remarkGfm from "remark-gfm"; +import { makeStyles } from "@material-ui/core"; +import { colors } from "@postgres.ai/shared/styles/colors"; +import { icons } from "@postgres.ai/shared/styles/icons"; +import { DebugDialog } from "../../DebugDialog/DebugDialog"; +import { CodeBlock } from "./CodeBlock"; +import { permalinkLinkBuilder } from "../../utils"; + +type BaseMessageProps = { + id: string | null; + created_at?: string; + content?: string; + name?: string; + isLoading?: boolean; + formattedTime?: string +} + +type AiMessageProps = BaseMessageProps & { + isAi: true; + content: string; +} + +type HumanMessageProps = BaseMessageProps & { + isAi: false; + name: string; + content: string +} + +type LoadingMessageProps = BaseMessageProps & { + isLoading: true; + isAi: true; + content?: undefined +} + +type MessageProps = AiMessageProps | HumanMessageProps | LoadingMessageProps; + +const useStyles = makeStyles( + (theme) => ({ + message: { + padding: 10, + paddingLeft: 60, + position: 'relative', + whiteSpace: 'normal', + [theme.breakpoints.down('xs')]: { + paddingLeft: 30 + }, + '& .markdown pre': { + [theme.breakpoints.down('sm')]: { + display: 'inline-block', + minWidth: '100%', + width: 'auto', + }, + [theme.breakpoints.up('md')]: { + display: 'block', + maxWidth: 'auto', + width: 'auto', + }, + [theme.breakpoints.up('lg')]: { + display: 'block', + maxWidth: 'auto', + width: 'auto', + }, + }, + }, + messageAvatar: { + top: '10px', + left: '15px', + position: 'absolute', + width: 30, + height: 30, + [theme.breakpoints.down('xs')]: { + width: 24, + height: 24, + left: 0, + '& svg': { + width: 24, + height: 24, + } + } + }, + messageAvatarImage: { + width: '100%', + borderRadius: '50%' + }, + messageAuthor: { + fontSize: 14, + fontWeight: 'bold', + }, + messageInfo: { + display: 'inline-block', + marginLeft: 10, + padding: 0, + fontSize: '0.75rem', + color: colors.pgaiDarkGray, + transition: '.2s ease', + background: "none", + border: "none", + textDecoration: "none", + '@media (max-width: 450px)': { + '&:nth-child(1)': { + display: 'none' + } + } + }, + messageInfoActive: { + borderBottom: '1px solid currentcolor', + cursor: 'pointer', + '&:hover': { + color: '#404040' + } + }, + messageHeader: { + height: '1.125rem', + display: 'flex', + flexWrap: 'wrap', + alignItems: 'baseline', + '@media (max-width: 450px)': { + height: '2.25rem', + } + }, + additionalInfo: { + '@media (max-width: 450px)': { + width: '100%', + marginTop: 4, + marginLeft: -10, + + } + }, + messagesSpinner: { + display: 'flex', + justifyContent: 'center', + padding: 10 + }, + markdown: { + margin: '5px 5px 5px 0', + fontSize: 14, + '& h1': { + marginTop: 5 + }, + '& table': { + borderCollapse: 'collapse', + borderSpacing: 0 + }, + '& tr': { + borderTop: '1px solid #c6cbd1', + background: '#fff' + }, + '& th, & td': { + padding: '10px 13px', + border: '1px solid #dfe2e5' + }, + '& table tr:nth-child(2n)': { + background: '#f6f8fa' + }, + + '& blockquote': { + color: '#666', + margin: 0, + paddingLeft: '3em', + borderLeft: '0.5em #eee solid' + }, + '& img.emoji': { + marginTop: 5 + }, + '& code': { + border: '1px dotted silver', + display: 'inline-block', + borderRadius: 3, + padding: 2, + backgroundColor: '#f6f8fa', + marginBottom: 3, + fontSize: '13px !important', + fontFamily: "'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New'," + + " 'andale mono', 'lucida console', monospace", + }, + '& pre code': { + background: 'none', + border: 0, + margin: 0, + borderRadius: 0, + display: 'inline', + padding: 0, + }, + '& div:not([class]):not([role])': { + display: 'block', + marginBlockStart: '1em', + marginBlockEnd: '1em', + marginInlineStart: 0, + marginInlineEnd: 0, + }, + '& .MuiExpansionPanel-root div': { + marginBlockStart: 0, + marginBlockEnd: 0, + }, + }, + loading: { + display: 'block', + marginBlockStart: '1em', + marginBlockEnd: '1em', + marginInlineStart: 0, + marginInlineEnd: 0, + fontSize: 14, + '&:after': { + overflow: 'hidden', + display: 'inline-block', + verticalAlign: 'bottom', + animation: '$ellipsis steps(4,end) 1.2s infinite', + content: "'\\2026'", + width: 0, + } + }, + '@keyframes ellipsis': { + 'to': { + width: '0.9em' + }, + } + }), + +) + +export const Message = React.memo((props: MessageProps) => { + const { + id, + isAi, + formattedTime, + content, + name, + created_at, + isLoading + } = props; + + const [isDebugVisible, setDebugVisible] = useState(false); + + const classes = useStyles(); + + const toggleDebugDialog = () => { + setDebugVisible(prevState => !prevState) + } + + const contentToRender: string = content?.replace(/\n/g, ' \n') || '' + + return ( + <> + +
+
+ {isAi + ? Postgres.AI Bot avatar + : icons.userChatIcon} +
+
+
+ {isLoading + ?
+
+ Thinking +
+
+ : , + code: ({ node, inline, className, children, ...props }) => { + const match = /language-(\w+)/.exec(className || ''); + return !inline ? ( + + ) : ( + {children} + ); + }, + }} + /> + } +
+
+ + ) +}) \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx new file mode 100644 index 00000000..756d29e4 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx @@ -0,0 +1,288 @@ +/*-------------------------------------------------------------------------- + * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai + * All Rights Reserved. Proprietary and confidential. + * Unauthorized copying of this file, via any medium is strictly prohibited + *-------------------------------------------------------------------------- + */ + +import { useRef, useEffect, useState } from 'react'; +import { makeStyles, Typography } from "@material-ui/core"; +import cn from "classnames"; +import { ResizeObserver } from '@juggle/resize-observer'; +import {colors} from "@postgres.ai/shared/styles/colors"; +import {PageSpinner} from "@postgres.ai/shared/components/PageSpinner"; +import { usePrev } from 'hooks/usePrev'; +import { getMaxScrollTop, getUserMessagesCount } from './utils'; +import Format from "../../../utils/format"; +import {BotMessage} from "../../../types/api/entities/bot"; +import { Message } from "./Message/Message"; + +const useStyles = makeStyles( + (theme) => ({ + root: { + borderRadius: 4, + overflow: 'hidden', + flex: '1 0 160px', + display: 'flex', + flexDirection: 'column', + }, + emptyChat: { + justifyContent: 'center', + alignItems: 'center', + textAlign: 'center' + }, + emptyChatMessage: { + maxWidth: '80%', + fontSize: '0.875rem' + }, + messages: { + overflowY: 'auto', + flex: '1 1 100%' + }, + message: { + padding: 10, + paddingLeft: 60, + position: 'relative', + whiteSpace: 'normal', + [theme.breakpoints.down('xs')]: { + paddingLeft: 30 + }, + '& .markdown pre': { + [theme.breakpoints.down('sm')]: { + display: 'inline-block', + minWidth: '100%', + width: 'auto', + }, + [theme.breakpoints.up('md')]: { + display: 'block', + maxWidth: 'auto', + width: 'auto', + }, + [theme.breakpoints.up('lg')]: { + display: 'block', + maxWidth: 'auto', + width: 'auto', + }, + }, + }, + messageAvatar: { + top: '10px', + left: '15px', + position: 'absolute', + width: 30, + height: 30, + [theme.breakpoints.down('xs')]: { + width: 24, + height: 24, + left: 0, + '& svg': { + width: 24, + height: 24, + } + } + }, + messageAvatarImage: { + width: '100%', + borderRadius: '50%' + }, + messageAuthor: { + fontSize: 14, + fontWeight: 'bold', + }, + messageInfo: { + display: 'inline-block', + marginLeft: 10, + fontSize: '0.75rem', + color: colors.pgaiDarkGray, + transition: '.2s ease' + }, + messageInfoActive: { + '&:hover': { + color: '#404040' + } + }, + messageHeader: { + height: '1.125rem', + }, + messagesSpinner: { + display: 'flex', + justifyContent: 'center', + padding: 10 + } + }), + +) + +type MessagesProps = { + messages: BotMessage[] | null + isLoading: boolean + isWaitingForAnswer: boolean +} + +type Time = string + +type FormattedTime = { + [id: string]: Time +} + +export const Messages = (props: MessagesProps) => { + const { + messages, + isLoading, + isWaitingForAnswer + } = props; + + const rootRef = useRef(null); + const wrapperRef = useRef(null); + const atBottomRef = useRef(true); + const shouldSkipScrollCalcRef = useRef(false); + const classes = useStyles(); + const [formattedTimes, setFormattedTimes] = useState({}); + + // Scroll handlers. + const scrollBottom = () => { + shouldSkipScrollCalcRef.current = true; + if (rootRef.current) { + rootRef.current.scrollTop = getMaxScrollTop(rootRef.current); + } + atBottomRef.current = true; + }; + + const scrollBottomIfNeed = () => { + if (!atBottomRef.current) { + return; + } + + scrollBottom(); + }; + + // Listening resizing of wrapper. + useEffect(() => { + const observedElement = wrapperRef.current; + if (!observedElement) return; + + const resizeObserver = new ResizeObserver(scrollBottomIfNeed); + resizeObserver.observe(observedElement); + + return () => resizeObserver.unobserve(observedElement); + }, [wrapperRef.current]); + + // Scroll to bottom if user sent new message. + const userMessagesCount = getUserMessagesCount(messages || [] as BotMessage[]); + const prevUserMessagesCount = usePrev(userMessagesCount); + + useEffect(() => { + if ((userMessagesCount > (prevUserMessagesCount || 0)) && rootRef.current) { + scrollBottom(); + } + }, [prevUserMessagesCount, userMessagesCount]); + + useEffect(() => { + if (!isLoading) { + scrollBottomIfNeed(); + } + }, [isLoading, scrollBottomIfNeed]); + + useEffect(() => { + const updateTimes = () => { + if (messages && messages.length > 0) { + const newFormattedTimes: FormattedTime = {}; + messages.forEach(message => { + newFormattedTimes[message.id] = Format.timeAgo(message.created_at) || ''; + }); + setFormattedTimes(newFormattedTimes); + } + }; + + updateTimes(); + + const intervalId = setInterval(updateTimes, 60000); + + return () => clearInterval(intervalId); + }, [messages]); + + // Check auto-scroll condition. + const calcIsAtBottom = () => { + if (shouldSkipScrollCalcRef.current) { + shouldSkipScrollCalcRef.current = false; + return; + } + if (rootRef.current) { + atBottomRef.current = rootRef.current.scrollTop >= getMaxScrollTop(rootRef.current); + } + }; + + if (isLoading) { + return ( +
+ +
+ ) + } + + if (!messages || messages.length === 0) { + return ( +
+ + Postgres.AI Bot can make mistakes.
+ Consider checking important information.
+ Depending on settings, LLM service provider such as GCP or OpenAI is used. +
+
+ ) + } + + return ( +
+
+
+ {messages && + Object.keys(messages).map((m) => { + const idx = Number(m) + + const { + id, + is_ai, + last_name, + first_name, + display_name, + slack_profile, + created_at, + content + } = messages[idx]; + let name = 'You'; + + if (first_name || last_name) { + name = `${first_name || ''} ${last_name || ''}`.trim(); + } else if (display_name) { + name = display_name; + } else if (slack_profile) { + name = slack_profile; + } + + let formattedTime = ''; + + if (formattedTimes) { + formattedTime = formattedTimes[id] + } + + return ( + + ) + })} + {isWaitingForAnswer && + + } +
+
+
+ ); +}; diff --git a/ui/packages/platform/src/pages/Bot/Messages/utils.ts b/ui/packages/platform/src/pages/Bot/Messages/utils.ts new file mode 100644 index 00000000..c10dc2e1 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Messages/utils.ts @@ -0,0 +1,25 @@ +/*-------------------------------------------------------------------------- + * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai + * All Rights Reserved. Proprietary and confidential. + * Unauthorized copying of this file, via any medium is strictly prohibited + *-------------------------------------------------------------------------- + */ + +import {BotMessage} from "../../../types/api/entities/bot"; + +export const getMaxScrollTop = (element: HTMLElement) => + element.scrollHeight - element.clientHeight + + +export const getUserMessagesCount = (messages: BotMessage[]) => { + if (!messages) { + return 0 + } + + const keys = Object.keys(messages) + + return keys.reduce((count, key) => { + const idx = Number(key) + return !messages[idx].is_ai ? count + 1 : count + }, 0) +} diff --git a/ui/packages/platform/src/pages/Bot/PublicChatDialog/PublicChatDialog.tsx b/ui/packages/platform/src/pages/Bot/PublicChatDialog/PublicChatDialog.tsx new file mode 100644 index 00000000..53018e37 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/PublicChatDialog/PublicChatDialog.tsx @@ -0,0 +1,286 @@ +/*-------------------------------------------------------------------------- + * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai + * All Rights Reserved. Proprietary and confidential. + * Unauthorized copying of this file, via any medium is strictly prohibited + *-------------------------------------------------------------------------- + */ + +import React, { useState } from 'react' +import { + IconButton, + TextField, + Dialog, + Typography, + Radio, + RadioGroup, + FormControlLabel, + Button, + makeStyles, +} from '@material-ui/core' +import MuiDialogTitle from '@material-ui/core/DialogTitle' +import MuiDialogContent from '@material-ui/core/DialogContent' +import MuiDialogActions from '@material-ui/core/DialogActions' + +import { styles } from '@postgres.ai/shared/styles/styles' +import { icons } from '@postgres.ai/shared/styles/icons' +import { Spinner } from '@postgres.ai/shared/components/Spinner' +import { colors } from "@postgres.ai/shared/styles/colors"; + +type DialogTitleProps = { + id: string + children: React.ReactNode + onClose: () => void +} + +type PublicChatDialogProps = { + defaultValue: 'public' | 'private' + isOpen: boolean + onClose: () => void + onSaveChanges: (value: boolean) => void + isLoading: boolean + threadId: string +} + +const useDialogTitleStyles = makeStyles( + (theme) => ({ + root: { + margin: 0, + padding: theme.spacing(1), + }, + dialogTitle: { + fontSize: 16, + lineHeight: '19px', + fontWeight: 600, + }, + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: 4, + color: theme.palette.grey[500], + }, + }), + { index: 1 }, +) + +const DialogTitle = (props: DialogTitleProps) => { + const classes = useDialogTitleStyles() + const { children, onClose, ...other } = props + return ( + + {children} + {onClose ? ( + + {icons.closeIcon} + + ) : null} + + ) +} + +const useDialogContentStyles = makeStyles( + (theme) => ({ + dialogContent: { + paddingTop: 10, + padding: theme.spacing(2), + }, + }), + { index: 1 }, +) + +const DialogContent = (props: { children: React.ReactNode }) => { + const classes = useDialogContentStyles() + return ( + + {props.children} + + ) +} + +const useDialogActionsStyles = makeStyles( + (theme) => ({ + root: { + margin: 0, + padding: theme.spacing(1), + }, + }), + { index: 1 }, +) + +const DialogActions = (props: { children: React.ReactNode }) => { + const classes = useDialogActionsStyles() + return ( + + {props.children} + + ) +} + +const useDialogStyles = makeStyles( + () => ({ + textField: { + ...styles.inputField, + marginTop: '0px', + width: 480, + }, + copyButton: { + marginTop: '-3px', + fontSize: '20px', + }, + dialog: {}, + remark: { + fontSize: 12, + lineHeight: '12px', + color: colors.state.warning, + paddingLeft: 20, + }, + remarkIcon: { + display: 'block', + height: '20px', + width: '22px', + float: 'left', + paddingTop: '5px', + }, + urlContainer: { + marginTop: 10, + paddingLeft: 22, + }, + radioLabel: { + fontSize: 12, + }, + dialogContent: { + paddingTop: 10, + }, + }), + { index: 1 }, +) + +export const PublicChatDialog = (props: PublicChatDialogProps) => { + const { onSaveChanges, defaultValue, onClose, isOpen, isLoading, threadId } = props; + + const [visibility, setVisibility] = useState(defaultValue ? "public" : "private"); + + const classes = useDialogStyles(); + + const publicUrl = `https://postgres.ai/chats/${threadId}`; + + const handleCopyUrl = () => { + if ('clipboard' in navigator) { + navigator.clipboard.writeText(publicUrl); + } + } + + const handleSaveChanges = () => { + if (defaultValue !== visibility) { + onSaveChanges(visibility === 'public'); + } + } + + const urlField = ( +
+ event.target.select()} + InputProps={{ + readOnly: true, + id: 'sharedUrl', + }} + InputLabelProps={{ + shrink: true, + style: styles.inputFieldLabel, + }} + FormHelperTextProps={{ + style: styles.inputFieldHelper, + }} + /> + + + {icons.copyIcon} + +
+ ) + + return ( + + + Public Chat + + + { + setVisibility(event.target.value) + }} + className={classes.radioLabel} + > + } + label="Only members of the organization can view" + /> + + } + label="Anyone with a special link and members of the organization can view" + /> + + {/*{shareUrl.remark && ( + + {icons.warningIcon} + {shareUrl.remark} + + )}*/} + {visibility && ( +
{urlField}
+ )} +
+ + + + +
+ ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/SettingsWithLabel/SettingsWithLabel.tsx b/ui/packages/platform/src/pages/Bot/SettingsWithLabel/SettingsWithLabel.tsx new file mode 100644 index 00000000..be5bf723 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/SettingsWithLabel/SettingsWithLabel.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import cn from "classnames"; +import { Button, makeStyles, useMediaQuery } from "@material-ui/core"; +import SettingsOutlinedIcon from "@material-ui/icons/SettingsOutlined"; +import { colors } from "@postgres.ai/shared/styles/colors"; +import { theme } from "@postgres.ai/shared/styles/theme"; +import { Link } from "react-router-dom"; +import { permalinkLinkBuilder } from "../utils"; + +type SettingsWithLabelProps = { + chatVisibility: 'private' | 'public'; + onSettingsClick: () => void; + permalinkId?: string +} + +const useStyles = makeStyles((theme) => ({ + label: { + backgroundColor: colors.primary.main, + color: colors.primary.contrastText, + display: 'inline-block', + borderRadius: 3, + fontSize: 10, + lineHeight: '12px', + padding: 2, + paddingLeft: 3, + paddingRight: 3, + verticalAlign: 'text-top', + marginRight: 8, + textDecoration: 'none' + }, + labelPrivate: { + backgroundColor: colors.pgaiDarkGray, + }, + disabled: { + pointerEvents: "none" + }, + button: { + [theme.breakpoints.down('sm')]: { + border: 'none', + minWidth: '2rem', + height: '2rem', + padding: 0, + marginLeft: '0.5rem', + '& .MuiButton-startIcon': { + margin: 0 + } + } + } + }), +) + +export const SettingsWithLabel = (props: SettingsWithLabelProps) => { + const { chatVisibility, onSettingsClick, permalinkId } = props; + const classes = useStyles(); + const matches = useMediaQuery(theme.breakpoints.down('sm')); + return ( + <> + + This thread is {chatVisibility} + + + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/hooks.ts b/ui/packages/platform/src/pages/Bot/hooks.ts new file mode 100644 index 00000000..3e0cf23d --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/hooks.ts @@ -0,0 +1,352 @@ +/*-------------------------------------------------------------------------- + * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai + * All Rights Reserved. Proprietary and confidential. + * Unauthorized copying of this file, via any medium is strictly prohibited + *-------------------------------------------------------------------------- + */ + +import {useCallback, useEffect, useRef, useState} from "react"; +import useWebSocket, {ReadyState} from "react-use-websocket"; +import { useLocation } from "react-router-dom"; +import {BotMessage} from "../../types/api/entities/bot"; +import {getChatsWithWholeThreads} from "../../api/bot/getChatsWithWholeThreads"; +import {getChats} from "api/bot/getChats"; +import {useAlertSnackbar} from "@postgres.ai/shared/components/AlertSnackbar/useAlertSnackbar"; +import {localStorage} from "../../helpers/localStorage"; +import { makeChatPublic } from "../../api/bot/makeChatPublic"; + + +const WS_URL = process.env.REACT_APP_WS_URL || ''; + +type ErrorType = { + code?: number; + message: string; + type?: 'connection' | 'chatNotFound'; +} + +type sendMessageType = { + content: string; + thread_id?: string | null; + org_id?: number | null; + is_public?: boolean; +} + +type UseAiBotReturnType = { + messages: BotMessage[] | null; + error: ErrorType | null; + loading: boolean; + sendMessage: (args: sendMessageType) => Promise; + clearChat: () => void; + wsLoading: boolean; + wsReadyState: ReadyState; + changeChatVisibility: (threadId: string, isPublic: boolean) => void; + isChangeVisibilityLoading: boolean; + unsubscribe: (threadId: string) => void +} + +type UseAiBotArgs = { + threadId?: string; + prevThreadId?: string; + onChatLoadingError?: () => void; +} + +export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { + const { threadId, onChatLoadingError } = args; + const { showMessage, closeSnackbar } = useAlertSnackbar(); + let location = useLocation<{skipReloading?: boolean}>(); + + const [messages, setMessages] = useState(null); + const [isLoading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [wsLoading, setWsLoading] = useState(false); + const [isChangeVisibilityLoading, setIsChangeVisibilityLoading] = useState(false) + + const token = localStorage.getAuthToken() + + const onWebSocketError = (error: WebSocketEventMap['error']) => { + console.error('WebSocket error:', error); + showMessage('WebSocket connection error: attempting to reconnect'); + } + + const onWebSocketMessage = (event: WebSocketEventMap['message']) => { + if (event.data) { + const messageData: BotMessage = JSON.parse(event.data); + if (messageData) { + if ((threadId && threadId === messageData.thread_id) || (!threadId && messageData.parent_id)) { + let currentMessages = [...(messages || [])]; + + // Check if the last message needs its data updated + const lastMessage = currentMessages[currentMessages.length - 1]; + if (lastMessage && !lastMessage.id && messageData.parent_id) { + lastMessage.id = messageData.parent_id; + lastMessage.created_at = messageData.created_at; + lastMessage.is_public = messageData.is_public; + } + + currentMessages.push(messageData); + setMessages(currentMessages); + if (document.visibilityState === "hidden") { + if (Notification.permission === "granted") { + new Notification("New message", { + body: 'New message from Postgres.AI Bot', + icon: '/images/bot_avatar.png' + }); + } + } + } + } else { + showMessage('An error occurred. Please try again') + } + } else { + showMessage('An error occurred. Please try again') + } + setWsLoading(false); + setLoading(false); + } + + const onWebSocketOpen = () => { + console.log('WebSocket connection established'); + if (threadId) { + subscribe(threadId) + } + setWsLoading(false); + setLoading(false); + closeSnackbar(); + } + const onWebSocketClose = (event: WebSocketEventMap['close']) => { + console.log('WebSocket connection closed', event); + showMessage('WebSocket connection error: attempting to reconnect'); + } + + const { sendMessage: wsSendMessage, readyState, } = useWebSocket(WS_URL, { + protocols: ['Authorization', token || ''], + shouldReconnect: () => true, + reconnectAttempts: 50, + reconnectInterval: 5000, // ms + onError: onWebSocketError, + onMessage: onWebSocketMessage, + onClose: onWebSocketClose, + onOpen: onWebSocketOpen + }) + + const getChatMessages = useCallback(async (threadId: string) => { + setError(null); + if (threadId) { + setLoading(true); + try { + const { response} = await getChatsWithWholeThreads({id: threadId}); + subscribe(threadId) + if (response && response.length > 0) { + setMessages(response); + } else { + if (onChatLoadingError) onChatLoadingError(); + setError({ + code: 404, + message: 'Specified chat not found or you have no access.', + type: 'chatNotFound' + }) + } + } catch (e) { + setError(e as unknown as ErrorType) + showMessage('Connection error') + } finally { + setLoading(false); + } + } + }, []); + + useEffect(() => { + let isCancelled = false; + setError(null); + setWsLoading(false); + + if (threadId && !location.state?.skipReloading) { + getChatMessages(threadId) + .catch((e) => { + if (!isCancelled) { + setError(e); + } + }); + } else if (threadId) { + subscribe(threadId) + } + + return () => { + isCancelled = true; + }; + }, [getChatMessages, location.state?.skipReloading, threadId]); + + useEffect(() => { + const fetchData = async () => { + if (threadId) { + const { response } = await getChatsWithWholeThreads({id: threadId}); + if (response && response.length > 0) { + setMessages(response); + } + } + }; + + let intervalId: NodeJS.Timeout | null = null; + + if (readyState === ReadyState.CLOSED) { + intervalId = setInterval(fetchData, 20000); + } + + return () => { + if (intervalId) { + clearInterval(intervalId); + } + }; + }, [readyState, threadId]); + + const sendMessage = async ({content, thread_id, org_id, is_public}: sendMessageType) => { + setWsLoading(true) + if (!thread_id) { + setLoading(true) + } + try { + //TODO: fix it + if (messages && messages.length > 0) { + setMessages((prevState) => [...prevState as BotMessage[], { content, is_ai: false, created_at: new Date().toISOString() } as BotMessage]) + } else { + setMessages([{ content, is_ai: false, created_at: new Date().toISOString() } as BotMessage]) + } + wsSendMessage(JSON.stringify({ + action: 'send', + payload: { + content, + thread_id, + org_id, + is_public + } + })) + setError(error) + + } catch (e) { + setError(e as unknown as ErrorType) + } finally { + setLoading(false) + } + } + + const clearChat = () => { + setMessages(null); + } + + const changeChatVisibility = async (threadId: string, isPublic: boolean) => { + setIsChangeVisibilityLoading(true) + try { + const { error } = await makeChatPublic({ + thread_id: threadId, + is_public: isPublic + }) + if (error) { + showMessage('Failed to change chat visibility. Please try again later.') + } else if (messages?.length) { + const newMessages: BotMessage[] = messages.map((message) => ({ + ...message, + is_public: isPublic + })) + setMessages(newMessages) + } + } catch (e) { + showMessage('Failed to change chat visibility. Please try again later.') + } finally { + setIsChangeVisibilityLoading(false) + } + } + + const subscribe = (threadId: string) => { + wsSendMessage(JSON.stringify({ + action: 'subscribe', + payload: { + thread_id: threadId, + } + })) + } + + const unsubscribe = (threadId: string) => { + wsSendMessage(JSON.stringify({ + action: 'unsubscribe', + payload: { + thread_id: threadId, + } + })) + } + + useEffect(() => { + if ('Notification' in window) { + Notification.requestPermission().then(permission => { + if (permission === "granted") { + console.log("Permission for notifications granted"); + } else { + console.log("Permission for notifications denied"); + } + }); + } + }, []) + + return { + error: error, + wsLoading: wsLoading, + wsReadyState: readyState, + loading: isLoading, + changeChatVisibility, + isChangeVisibilityLoading, + sendMessage, + clearChat, + messages, + unsubscribe + } +} + +type UseBotChatsListHook = { + chatsList: BotMessage[] | null; + error: Response | null; + loading: boolean; + getChatsList: () => void +}; + +export const useBotChatsList = (orgId?: number): UseBotChatsListHook => { + const [chatsList, setChatsList] = useState(null); + const [isLoading, setLoading] = useState(false); + const [error, setError] = useState(null) + + const getChatsList = useCallback(async () => { + setLoading(true); + try { + const queryString = `?parent_id=is.null&org_id=eq.${orgId}`; + const { response, error } = await getChats({ query: queryString }); + + setChatsList(response); + setError(error) + + } catch (e) { + setError(e as unknown as Response) + } finally { + setLoading(false) + } + }, []); + + useEffect(() => { + let isCancelled = false; + + getChatsList() + .catch((e) => { + if (!isCancelled) { + setError(e); + } + }); + + return () => { + isCancelled = true; + }; + }, [getChatsList]); + + return { + chatsList, + error, + getChatsList, + loading: isLoading + } +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/index.tsx b/ui/packages/platform/src/pages/Bot/index.tsx new file mode 100644 index 00000000..b9ee6c7f --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/index.tsx @@ -0,0 +1,264 @@ +/*-------------------------------------------------------------------------- + * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai + * All Rights Reserved. Proprietary and confidential. + * Unauthorized copying of this file, via any medium is strictly prohibited + *-------------------------------------------------------------------------- + */ + +import React, { useEffect, useState } from 'react'; +import cn from "classnames"; +import {ReadyState} from "react-use-websocket"; +import Box from '@mui/material/Box/Box'; +import { makeStyles, useMediaQuery } from "@material-ui/core"; +import { useHistory, useRouteMatch } from "react-router-dom"; +import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper'; +import { ErrorWrapper } from "../../components/Error/ErrorWrapper"; +import { Messages } from './Messages/Messages'; +import { Command } from './Command/Command'; +import { ChatsList } from "./ChatsList/ChatsList"; +import { BotWrapperProps } from "./BotWrapper"; +import { useBotChatsList, useAiBot } from "./hooks"; +import { usePrev } from "../../hooks/usePrev"; +import {HeaderButtons} from "./HeaderButtons/HeaderButtons"; +import settings from "../../utils/settings"; +import { PublicChatDialog } from "./PublicChatDialog/PublicChatDialog"; +import { theme } from "@postgres.ai/shared/styles/theme"; +import { colors } from "@postgres.ai/shared/styles/colors"; +import { SettingsWithLabel } from "./SettingsWithLabel/SettingsWithLabel"; + +type BotPageProps = BotWrapperProps; + +const useStyles = makeStyles( + (theme) => ({ + actions: { + display: 'flex', + alignItems: 'center', + alignSelf: 'flex-end', + marginTop: -20, + [theme.breakpoints.down('sm')]: { + marginTop: -22 + } + }, + hiddenButtons: { + width: 192, + marginLeft: 52, + [theme.breakpoints.down('sm')]: { + width: 226 + } + }, + toggleListButton: { + flex: '0 0 auto', + }, + contentContainer: { + height: '100%', + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + transition: theme.transitions.create('margin', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + marginRight: 4, + }, + isChatsListVisible: { + transition: theme.transitions.create('margin', { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + marginRight: 244, + [theme.breakpoints.down('sm')]: { + marginRight: 0, + } + }, + label: { + backgroundColor: colors.primary.main, + color: colors.primary.contrastText, + display: 'inline-block', + borderRadius: 3, + fontSize: 10, + lineHeight: '12px', + padding: 2, + paddingLeft: 3, + paddingRight: 3, + verticalAlign: 'text-top', + marginRight: 8 + }, + labelPrivate: { + backgroundColor: colors.pgaiDarkGray, + } + }), + { index: 1 }, +) + +export const BotPage = (props: BotPageProps) => { + const { match, project, orgData } = props; + + const { + messages, + loading, + error, + sendMessage, + clearChat, + wsLoading, + wsReadyState, + isChangeVisibilityLoading, + changeChatVisibility, + unsubscribe + } = useAiBot({ + threadId: match.params.threadId, + }); + const {chatsList, loading: chatsListLoading, getChatsList} = useBotChatsList(orgData.id); + + const matches = useMediaQuery(theme.breakpoints.down('sm')); + + const [isChatsListVisible, setChatsListVisible] = useState(window?.innerWidth > 640); + const [isVisibilityDialogVisible, setVisibilityDialogVisible] = useState(false); + const [chatVisibility, setChatVisibility] = useState<'public' | 'private'>('public'); + + const history = useHistory(); + + const prevThreadId = usePrev(match.params.threadId); + + const isDemoOrg = useRouteMatch(`/${settings.demoOrgAlias}`); + + const classes = useStyles(); + + const breadcrumbs = ( + + ); + + const toggleChatsList = () => { + setChatsListVisible((prevState) => !prevState) + } + + const toggleVisibilityDialog = () => { + setVisibilityDialogVisible((prevState) => !prevState) + } + + const handleSendMessage = async (message: string) => { + const { threadId } = match.params; + const orgId = orgData.id || null; + + await sendMessage({ + content: message, + thread_id: threadId || null, + org_id: orgId, + is_public: chatVisibility === 'public' + }) + } + + const handleCreateNewChat = () => { + clearChat(); + history.push(`/${match.params.org}/bot`); + } + + const handleSaveChatVisibility = (value: boolean) => { + if (match.params.threadId) { + changeChatVisibility(match.params.threadId, value) + getChatsList(); + } + } + + const handleChatListLinkClick = (targetThreadId: string) => { + if (match.params.threadId && match.params.threadId !== targetThreadId) { + unsubscribe(match.params.threadId) + } + } + + useEffect(() => { + if (!match.params.threadId && !prevThreadId && messages && messages.length > 1 && messages[1].parent_id) { + // hack that skip additional loading chats_ancestors_and_descendants + history.replace(`/${match.params.org}/bot/${messages[1].parent_id}`, { skipReloading: true }) + getChatsList(); + } else if (prevThreadId && !match.params.threadId) { + clearChat() + } + }, [match.params.threadId, match.params.org, messages, prevThreadId]); + + useEffect(() => { + if (messages && messages.length > 0 && match.params.threadId) { + setChatVisibility(messages[0].is_public ? 'public' : 'private') + } + }, [messages]); + + useEffect(() => { + // fixes hack with skipping additional loading chats_ancestors_and_descendants + history.replace({ state: {} }) + }, []); + + if (error && error.code === 404) { + return ( + <> + {breadcrumbs} + + + ) + } + + return ( + <> + {match.params.threadId && } + + + {match.params.threadId && !matches && + } + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/utils.ts b/ui/packages/platform/src/pages/Bot/utils.ts new file mode 100644 index 00000000..135835c7 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/utils.ts @@ -0,0 +1,14 @@ +/*-------------------------------------------------------------------------- + * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai + * All Rights Reserved. Proprietary and confidential. + * Unauthorized copying of this file, via any medium is strictly prohibited + *-------------------------------------------------------------------------- + */ + +import { API_URL_PREFIX } from "../../config/env"; + +export const permalinkLinkBuilder = (id: string): string => { + const apiUrl = process.env.REACT_APP_API_URL_PREFIX || API_URL_PREFIX; + const isV2API = /https?:\/\/.*v2\.postgres\.ai\b/.test(apiUrl); + return `https://${isV2API ? 'v2.' : ''}postgres.ai/chats/${id}`; +}; \ No newline at end of file diff --git a/ui/packages/platform/src/react-syntax-highlighter.d.ts b/ui/packages/platform/src/react-syntax-highlighter.d.ts new file mode 100644 index 00000000..5de75d31 --- /dev/null +++ b/ui/packages/platform/src/react-syntax-highlighter.d.ts @@ -0,0 +1,4 @@ +declare module 'react-syntax-highlighter/dist/esm/languages/prism/supported-languages' { + const languages: string[]; + export default languages; +} \ No newline at end of file diff --git a/ui/packages/platform/src/stores/store.js b/ui/packages/platform/src/stores/store.js index 2b3eea1d..52be1370 100644 --- a/ui/packages/platform/src/stores/store.js +++ b/ui/packages/platform/src/stores/store.js @@ -597,6 +597,35 @@ const Store = Reflux.createStore({ this.trigger(this.data); }, + //bot settings + + onUpdateAiBotSettingsFailed: function (error) { + this.data.orgProfile.isUpdating = false; + this.data.orgProfile.updateError = true; + this.data.orgProfile.updateErrorMessage = error.message; + this.trigger(this.data); + }, + + onUpdateAiBotSettingsProgressed: function (data) { + this.data.orgProfile.updateErrorFields = null; + this.data.orgProfile.isUpdating = true; + + this.trigger(this.data); + }, + + onUpdateAiBotSettingsCompleted: function (data) { + this.data.orgProfile.isUpdating = false; + this.data.orgProfile.updateErrorMessage = this.getError(data); + this.data.orgProfile.updateError = !!this.data.orgProfile.updateErrorMessage; + + if (!this.data.orgProfile.updateError && data.length > 0) { + this.data.orgProfile.updateErrorFields = null; + Actions.showNotification('AI Bot settings successfully saved.', 'success'); + } + + this.trigger(this.data); + }, + onCreateOrgFailed: function (error) { this.data.orgProfile.isUpdating = false; @@ -2968,4 +2997,5 @@ const Store = Reflux.createStore({ } }); + export default Store; diff --git a/ui/packages/platform/src/types/api/entities/bot.ts b/ui/packages/platform/src/types/api/entities/bot.ts new file mode 100644 index 00000000..be26e1d0 --- /dev/null +++ b/ui/packages/platform/src/types/api/entities/bot.ts @@ -0,0 +1,16 @@ + +export type BotMessage = { + id: string + created_at: string + parent_id: string | null + content: string + is_ai: boolean + is_public: boolean + first_name: string | null + last_name: string | null + display_name: string | null + slack_profile: string | null + user_id: string + org_id: string + thread_id: string +} \ No newline at end of file diff --git a/ui/packages/platform/src/utils/format.ts b/ui/packages/platform/src/utils/format.ts index 9d1aeb04..606fd3c9 100644 --- a/ui/packages/platform/src/utils/format.ts +++ b/ui/packages/platform/src/utils/format.ts @@ -262,6 +262,26 @@ const Format = { limitStr: function (str: string, limit: number) { return str.length > limit ? str.substr(0, limit).concat('…') : str }, + + timeAgo: function (date: string | Date): string | null { + const now = new Date(); + const past = new Date(date); + const diff = Math.abs(now.getTime() - past.getTime()); + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (seconds < 60) { + return `${seconds} seconds ago`; + } else if (minutes < 60) { + return `${minutes} minutes ago`; + } else if (hours < 24) { + return `${hours} hours ago`; + } else { + return `${days} days ago`; + } + } } export default Format diff --git a/ui/packages/platform/src/utils/urls.ts b/ui/packages/platform/src/utils/urls.ts index d1d3c4df..20f68e91 100644 --- a/ui/packages/platform/src/utils/urls.ts +++ b/ui/packages/platform/src/utils/urls.ts @@ -219,4 +219,10 @@ export default { uuid ) }, + + linkBotChat: function (props: PropsType, msgId: string) { + const basePath = this.getBasePath(props) + + return basePath + '/bot/' + msgId + }, } diff --git a/ui/packages/shared/components/AlertSnackbar/index.tsx b/ui/packages/shared/components/AlertSnackbar/index.tsx new file mode 100644 index 00000000..a1dcf8f7 --- /dev/null +++ b/ui/packages/shared/components/AlertSnackbar/index.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import Snackbar from '@mui/material/Snackbar'; +import Alert from '@mui/material/Alert'; +import {useAlertSnackbar} from "./useAlertSnackbar"; + +export const AlertSnackbar = () => { + const { snackbarMessage, closeSnackbar } = useAlertSnackbar(); + return ( + + + {snackbarMessage?.message} + + + ) +} \ No newline at end of file diff --git a/ui/packages/shared/components/AlertSnackbar/useAlertSnackbar.tsx b/ui/packages/shared/components/AlertSnackbar/useAlertSnackbar.tsx new file mode 100644 index 00000000..ed5cc4bc --- /dev/null +++ b/ui/packages/shared/components/AlertSnackbar/useAlertSnackbar.tsx @@ -0,0 +1,65 @@ +import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; +import {AlertSnackbar} from "./index"; + +type AlertSnackbarMessage = { + message: string, + key: number +} + +type AlertSnackbarContextType = { + snackbarMessage: AlertSnackbarMessage | null; + showMessage: (message: string) => void; + closeSnackbar: () => void; +} + +const AlertSnackbarContext = createContext(undefined); + +export const useAlertSnackbar = () => { + const context = useContext(AlertSnackbarContext); + if (context === undefined) { + throw new Error('useSnackbar must be used within a SnackbarProvider'); + } + return context; +}; + +type SnackbarProviderProps = { + children: React.ReactNode +} + +export const AlertSnackbarProvider = (props: SnackbarProviderProps) => { + const { children } = props; + const [snackbarMessage, setSnackbarMessage] = useState(null); + const isMounted = useRef(true); + + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + + const showMessage = useCallback((message: string) => { + if (isMounted.current) { + setSnackbarMessage({message, key: new Date().getTime()}); + } + }, []); + + const closeSnackbar = useCallback(() => { + if (isMounted.current) { + setSnackbarMessage(null); + } + }, []); + + const value = { + snackbarMessage, + showMessage, + closeSnackbar, + }; + + return ( + + {children} + + + ); + +} \ No newline at end of file diff --git a/ui/packages/shared/components/TextField/index.tsx b/ui/packages/shared/components/TextField/index.tsx index 211b6f96..b85e9e25 100644 --- a/ui/packages/shared/components/TextField/index.tsx +++ b/ui/packages/shared/components/TextField/index.tsx @@ -33,6 +33,7 @@ export type TextFieldProps = { type?: 'text' | 'password' error?: boolean placeholder?: string + onBlur?: TextFieldPropsBase['onBlur'] } const useStyles = makeStyles( @@ -96,6 +97,7 @@ export const TextField = (props: TextFieldProps) => { type={props.type} error={props.error} placeholder={props.placeholder} + onBlur={props.onBlur} /> ) } diff --git a/ui/packages/shared/styles/icons.tsx b/ui/packages/shared/styles/icons.tsx index c0e722dd..fd8da26c 100644 --- a/ui/packages/shared/styles/icons.tsx +++ b/ui/packages/shared/styles/icons.tsx @@ -1894,4 +1894,10 @@ export const icons = { ), + aiBotIcon: ( + + + + ) } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index ea6c1676..70c31a78 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -410,6 +410,9 @@ importers: react-syntax-highlighter: specifier: ^15.5.0 version: 15.5.0(react@17.0.2) + react-use-websocket: + specifier: 3.0.0 + version: 3.0.0(react-dom@17.0.2)(react@17.0.2) reflux: specifier: ^6.4.1 version: 6.4.1(react@17.0.2) @@ -12396,6 +12399,16 @@ packages: react-dom: 17.0.2(react@17.0.2) dev: false + /react-use-websocket@3.0.0(react-dom@17.0.2)(react@17.0.2): + resolution: {integrity: sha512-BInlbhXYrODBPKIplDAmI0J1VPM+1KhCLN09o+dzgQ8qMyrYs4t5kEYmCrTqyRuMTmpahylHFZWQXpfYyDkqOw==} + peerDependencies: + react: '>= 16.8.0' + react-dom: '>= 16.8.0' + dependencies: + react: 17.0.2 + react-dom: 17.0.2(react@17.0.2) + dev: false + /react@17.0.2: resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==} engines: {node: '>=0.10.0'} From b54a04eef357829a13ab0eb5afbc9c4325c54435 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 17 May 2024 00:17:20 +0700 Subject: [PATCH 024/111] change url to bot websocket --- ui/packages/platform/deploy/configs/production.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/platform/deploy/configs/production.sh b/ui/packages/platform/deploy/configs/production.sh index abd2e0dd..9a23b907 100644 --- a/ui/packages/platform/deploy/configs/production.sh +++ b/ui/packages/platform/deploy/configs/production.sh @@ -23,4 +23,4 @@ export REACT_APP_STRIPE_API_KEY="xxx" export REACT_APP_SENTRY_DSN="https://91517477289e477cb8880f2f07a82632@sentry.postgres.ai/2" # AI Bot -export REACT_APP_WS_URL="wss://postgres.ai/ai-bot-wss/" # don't forget trailing slash! \ No newline at end of file +export REACT_APP_WS_URL="wss://postgres.ai/ai-bot-ws/" # don't forget trailing slash! \ No newline at end of file From b34d7d84336d122ca0767c15eb26ae4ae90e3159 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 24 May 2024 16:03:18 +0000 Subject: [PATCH 025/111] Bot UI: Enhance UX on mobile --- .../components/ContentLayout/Footer/index.tsx | 4 +- .../src/pages/Bot/ChatsList/ChatsList.tsx | 14 +++++-- .../src/pages/Bot/Command/Command.tsx | 28 ++++++++++---- .../platform/src/pages/Bot/Command/utils.ts | 18 +++++++-- ui/packages/platform/src/utils/utils.ts | 38 +++++++++++++++++++ .../shared/components/TextField/index.tsx | 2 + 6 files changed, 87 insertions(+), 17 deletions(-) diff --git a/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx b/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx index c781f85a..d1671c40 100644 --- a/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx +++ b/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx @@ -68,7 +68,7 @@ export const Footer = () => { const classes = useStyles() return ( -
+
{new Date().getFullYear()} © Postgres.AI
@@ -103,6 +103,6 @@ export const Footer = () => {
-
+ ) } diff --git a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx index 4bb0fa3b..a199c3d5 100644 --- a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx +++ b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx @@ -9,7 +9,7 @@ import React from "react"; import { Link } from "react-router-dom"; import { useParams } from "react-router"; import cn from "classnames"; -import { makeStyles, Theme } from "@material-ui/core"; +import { makeStyles, Theme, useMediaQuery } from "@material-ui/core"; import Drawer from '@material-ui/core/Drawer'; import List from "@material-ui/core/List"; import Divider from "@material-ui/core/Divider"; @@ -18,6 +18,7 @@ import Box from "@mui/material/Box"; import { Spinner } from "@postgres.ai/shared/components/Spinner"; import { HeaderButtons, HeaderButtonsProps } from "../HeaderButtons/HeaderButtons"; import { BotMessage } from "../../../types/api/entities/bot"; +import { theme } from "@postgres.ai/shared/styles/theme"; const useStyles = makeStyles((theme) => ({ @@ -111,7 +112,7 @@ export const ChatsList = (props: ChatsListProps) => { } = props; const classes = useStyles(props); const params = useParams<{ org?: string, threadId?: string }>(); - + const matches = useMediaQuery(theme.breakpoints.down('sm')); const linkBuilder = (msgId: string) => { if (params.org) { return `/${params.org}/bot/${msgId}` @@ -126,6 +127,12 @@ export const ChatsList = (props: ChatsListProps) => { } } + const handleCloseOnClickOutside = () => { + if (matches) { + onClose() + } + } + const loader = ( @@ -172,11 +179,12 @@ export const ChatsList = (props: ChatsListProps) => { return ( { const { sendDisabled, onSend, threadId } = props const classes = useStyles() - + const isMobile = isMobileDevice(); // Handle value. const [value, setValue] = useState('') @@ -104,11 +105,21 @@ export const Command = React.memo((props: Props) => { } const handleBlur = () => { - if (window.innerWidth < theme.breakpoints.values.sm) - window.scrollTo({ - top: 0, - behavior: 'smooth' - }) + if ((window.innerWidth < theme.breakpoints.values.sm) && isMobile) { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }) + const footer: HTMLElement | null = document.querySelector("footer") + if (footer) footer.style.display = 'flex'; + } + } + + const handleFocus = () => { + if ((window.innerWidth < theme.breakpoints.values.sm) && isMobile) { + const footer: HTMLElement | null = document.querySelector("footer") + if (footer) footer.style.display = 'none'; + } } const handleKeyDown = (e: React.KeyboardEvent) => { @@ -163,11 +174,13 @@ export const Command = React.memo((props: Props) => { return (
theme.breakpoints.values.sm} multiline className={classes.field} onKeyDown={handleKeyDown} + onChange={handleChange} onBlur={handleBlur} + onFocus={handleFocus} InputProps={{ inputRef, classes: { @@ -175,7 +188,6 @@ export const Command = React.memo((props: Props) => { }, }} value={value} - onChange={handleChange} placeholder="Message..." /> - e.code === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey +import { isMobileDevice } from "../../../utils/utils"; -export const checkIsNewLineCmd = (e: KeyboardEvent) => - e.code === 'Enter' && (e.shiftKey || e.ctrlKey || e.metaKey) +export const checkIsSendCmd = (e: KeyboardEvent): boolean => { + if (isMobileDevice()) { + return false; // On mobile devices, Enter should not send. + } + return e.code === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey; +}; + +export const checkIsNewLineCmd = (e: KeyboardEvent): boolean => { + if (isMobileDevice()) { + return e.code === 'Enter'; // On mobile devices, Enter should create a new line. + } + return e.code === 'Enter' && (e.shiftKey || e.ctrlKey || e.metaKey); +}; export const addNewLine = ( value: string, diff --git a/ui/packages/platform/src/utils/utils.ts b/ui/packages/platform/src/utils/utils.ts index def1e4ef..8707bef6 100644 --- a/ui/packages/platform/src/utils/utils.ts +++ b/ui/packages/platform/src/utils/utils.ts @@ -31,3 +31,41 @@ export const validateDLEName = (name: string) => { !name.match(/^([a-z](?:[-a-z0-9]{0,61}[a-z0-9])?|[1-9][0-9]{0,19})$/) ) } + +export const isMobileDevice = (): boolean => { + let hasTouchScreen = false; + + // Check for modern touch screen devices using maxTouchPoints + if ("maxTouchPoints" in navigator) { + hasTouchScreen = navigator.maxTouchPoints > 0; + } + // Check for older versions of IE with msMaxTouchPoints + else if ("msMaxTouchPoints" in navigator) { + hasTouchScreen = (navigator as unknown as { msMaxTouchPoints: number }).msMaxTouchPoints > 0; + } + // Use matchMedia to check for coarse pointer devices + else { + const mQ = window.matchMedia("(pointer:coarse)"); + if (mQ && mQ.media === "(pointer:coarse)") { + hasTouchScreen = mQ.matches; + } + // Check for the presence of the orientation property as a fallback (deprecated in modern browsers) + else if ('orientation' in window) { + hasTouchScreen = true; + } + // Last resort: fallback with user agent sniffing + else { + const UA = navigator.userAgent; + hasTouchScreen = ( + /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) || + /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA) + ); + } + } + + // Check for mobile screen width, 1366 because of iPad Pro in Landscape mode + // If this is not necessary, may reduce value to 1024 or 768 + const isMobileScreen = window.innerWidth <= 1366; + + return hasTouchScreen && isMobileScreen; +} \ No newline at end of file diff --git a/ui/packages/shared/components/TextField/index.tsx b/ui/packages/shared/components/TextField/index.tsx index b85e9e25..9500eb3f 100644 --- a/ui/packages/shared/components/TextField/index.tsx +++ b/ui/packages/shared/components/TextField/index.tsx @@ -34,6 +34,7 @@ export type TextFieldProps = { error?: boolean placeholder?: string onBlur?: TextFieldPropsBase['onBlur'] + onFocus?: TextFieldPropsBase['onFocus'] } const useStyles = makeStyles( @@ -98,6 +99,7 @@ export const TextField = (props: TextFieldProps) => { error={props.error} placeholder={props.placeholder} onBlur={props.onBlur} + onFocus={props.onFocus} /> ) } From b15a9a870a42172573a0f560302e7b9e0a3dc680 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 24 May 2024 16:03:46 +0000 Subject: [PATCH 026/111] Bot UI: ensure responses are sent to the correct thread --- ui/packages/platform/src/pages/Bot/hooks.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/hooks.ts b/ui/packages/platform/src/pages/Bot/hooks.ts index 3e0cf23d..160b984f 100644 --- a/ui/packages/platform/src/pages/Bot/hooks.ts +++ b/ui/packages/platform/src/pages/Bot/hooks.ts @@ -72,9 +72,8 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { if (event.data) { const messageData: BotMessage = JSON.parse(event.data); if (messageData) { - if ((threadId && threadId === messageData.thread_id) || (!threadId && messageData.parent_id)) { + if ((threadId && threadId === messageData.thread_id) || (!threadId && messageData.parent_id && messages)) { let currentMessages = [...(messages || [])]; - // Check if the last message needs its data updated const lastMessage = currentMessages[currentMessages.length - 1]; if (lastMessage && !lastMessage.id && messageData.parent_id) { From dd79d0c023046648f64f791994ebfff6320c296f Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 24 May 2024 16:04:33 +0000 Subject: [PATCH 027/111] Bot UI: Loading and render improvements --- .../pages/Bot/Messages/Message/CodeBlock.tsx | 11 +++--- .../pages/Bot/Messages/Message/Message.tsx | 34 ++++++++++--------- ui/packages/platform/src/pages/Bot/hooks.ts | 1 - 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx index 5d2289b5..47bfb054 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { memo, useState } from 'react'; import { Accordion, AccordionDetails, AccordionSummary, Typography, makeStyles, Button } from '@material-ui/core'; import { Prism as SyntaxHighlighter, SyntaxHighlighterProps } from 'react-syntax-highlighter' import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism' @@ -65,7 +65,9 @@ const useStyles = makeStyles((theme) => ({ } })); -export const CodeBlock = ({ value, language }: { value: string, language?: string | null }) => { +type CodeBlockProps = { value: string, language?: string | null }; + +export const CodeBlock = memo(({ value, language }: CodeBlockProps) => { const classes = useStyles(); const [expanded, setExpanded] = useState(false); const [copied, setCopied] = useState(false); @@ -92,8 +94,7 @@ export const CodeBlock = ({ value, language }: { value: string, language?: strin }); } }; - - + const header = (
{isValidLanguage && {language}} @@ -140,4 +141,4 @@ export const CodeBlock = ({ value, language }: { value: string, language?: strin />
); -} \ No newline at end of file +}) \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx index a2e59623..62094ffe 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react' +import React, { useMemo, useState } from 'react' import cn from "classnames"; -import ReactMarkdown from "react-markdown"; +import ReactMarkdown, { Components } from "react-markdown"; import rehypeRaw from "rehype-raw"; import remarkGfm from "remark-gfm"; import { makeStyles } from "@material-ui/core"; @@ -241,7 +241,20 @@ export const Message = React.memo((props: MessageProps) => { setDebugVisible(prevState => !prevState) } - const contentToRender: string = content?.replace(/\n/g, ' \n') || '' + const contentToRender = useMemo(() => content, [content]); + + const renderers = useMemo(() => ({ + p: ({ node, ...props }) =>
, + img: ({ node, ...props }) => , + code: ({ node, inline, className, children, ...props }) => { + const match = /language-(\w+)/.exec(className || ''); + return !inline ? ( + + ) : ( + {children} + ); + }, + }), []); return ( <> @@ -303,22 +316,11 @@ export const Message = React.memo((props: MessageProps) => {
: , - code: ({ node, inline, className, children, ...props }) => { - const match = /language-(\w+)/.exec(className || ''); - return !inline ? ( - - ) : ( - {children} - ); - }, - }} + components={renderers} /> }
diff --git a/ui/packages/platform/src/pages/Bot/hooks.ts b/ui/packages/platform/src/pages/Bot/hooks.ts index 3e0cf23d..45837047 100644 --- a/ui/packages/platform/src/pages/Bot/hooks.ts +++ b/ui/packages/platform/src/pages/Bot/hooks.ts @@ -110,7 +110,6 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { subscribe(threadId) } setWsLoading(false); - setLoading(false); closeSnackbar(); } const onWebSocketClose = (event: WebSocketEventMap['close']) => { From abf835cf7f703ecb8d88ab0127b7b7bf03ccde46 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Mon, 10 Jun 2024 15:20:57 +0000 Subject: [PATCH 028/111] Bot UI: ignore unsafe HTML tags in Markdown renderer --- .../src/pages/Bot/DebugDialog/DebugDialog.tsx | 3 +++ .../pages/Bot/Messages/Message/Message.tsx | 4 +++- ui/packages/platform/src/pages/Bot/utils.ts | 20 ++++++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx b/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx index 0ee4ac38..a752214d 100644 --- a/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx +++ b/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx @@ -5,6 +5,7 @@ import { DialogContent, IconButton, makeStyles, Typography } from "@material-ui/ import ReactMarkdown from "react-markdown"; import Format from "../../../utils/format"; import { icons } from "@postgres.ai/shared/styles/icons"; +import { disallowedHtmlTagsForMarkdown } from "../utils"; type DebugDialogProps = { isOpen: boolean; @@ -80,6 +81,8 @@ export const DebugDialog = (props: DebugDialogProps) => { components={{ p: 'div', }} + disallowedElements={disallowedHtmlTagsForMarkdown} + unwrapDisallowed > {debugMessage.message} diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx index 62094ffe..919a3f90 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx @@ -8,7 +8,7 @@ import { colors } from "@postgres.ai/shared/styles/colors"; import { icons } from "@postgres.ai/shared/styles/icons"; import { DebugDialog } from "../../DebugDialog/DebugDialog"; import { CodeBlock } from "./CodeBlock"; -import { permalinkLinkBuilder } from "../../utils"; +import { disallowedHtmlTagsForMarkdown, permalinkLinkBuilder } from "../../utils"; type BaseMessageProps = { id: string | null; @@ -321,6 +321,8 @@ export const Message = React.memo((props: MessageProps) => { remarkPlugins={[remarkGfm]} linkTarget='_blank' components={renderers} + disallowedElements={disallowedHtmlTagsForMarkdown} + unwrapDisallowed /> } diff --git a/ui/packages/platform/src/pages/Bot/utils.ts b/ui/packages/platform/src/pages/Bot/utils.ts index 135835c7..ecf04052 100644 --- a/ui/packages/platform/src/pages/Bot/utils.ts +++ b/ui/packages/platform/src/pages/Bot/utils.ts @@ -11,4 +11,22 @@ export const permalinkLinkBuilder = (id: string): string => { const apiUrl = process.env.REACT_APP_API_URL_PREFIX || API_URL_PREFIX; const isV2API = /https?:\/\/.*v2\.postgres\.ai\b/.test(apiUrl); return `https://${isV2API ? 'v2.' : ''}postgres.ai/chats/${id}`; -}; \ No newline at end of file +}; + +export const disallowedHtmlTagsForMarkdown= [ + 'script', + 'style', + 'iframe', + 'form', + 'input', + 'link', + 'meta', + 'embed', + 'object', + 'applet', + 'base', + 'frame', + 'frameset', + 'audio', + 'video', +] \ No newline at end of file From 201893e8c7f81b46227949aecbc52d3309f9513a Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Mon, 10 Jun 2024 21:41:35 +0000 Subject: [PATCH 029/111] BotUI: UI/UX improvements --- .../src/pages/Bot/Command/Command.tsx | 1 + ui/packages/platform/src/pages/Bot/hooks.ts | 31 +++++++++++++------ ui/packages/platform/src/pages/Bot/index.tsx | 7 +++-- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/Command/Command.tsx b/ui/packages/platform/src/pages/Bot/Command/Command.tsx index 4a72ee30..8ea86b61 100644 --- a/ui/packages/platform/src/pages/Bot/Command/Command.tsx +++ b/ui/packages/platform/src/pages/Bot/Command/Command.tsx @@ -168,6 +168,7 @@ export const Command = React.memo((props: Props) => { useEffect(() => { if (!inputRef.current) return if (window.innerWidth > theme.breakpoints.values.md) inputRef.current.focus() + setValue('') }, [threadId]); diff --git a/ui/packages/platform/src/pages/Bot/hooks.ts b/ui/packages/platform/src/pages/Bot/hooks.ts index a908cdbc..bc3846ca 100644 --- a/ui/packages/platform/src/pages/Bot/hooks.ts +++ b/ui/packages/platform/src/pages/Bot/hooks.ts @@ -14,6 +14,7 @@ import {getChats} from "api/bot/getChats"; import {useAlertSnackbar} from "@postgres.ai/shared/components/AlertSnackbar/useAlertSnackbar"; import {localStorage} from "../../helpers/localStorage"; import { makeChatPublic } from "../../api/bot/makeChatPublic"; +import { usePrev } from "../../hooks/usePrev"; const WS_URL = process.env.REACT_APP_WS_URL || ''; @@ -41,26 +42,34 @@ type UseAiBotReturnType = { wsReadyState: ReadyState; changeChatVisibility: (threadId: string, isPublic: boolean) => void; isChangeVisibilityLoading: boolean; - unsubscribe: (threadId: string) => void + unsubscribe: (threadId: string) => void; + chatsList: UseBotChatsListHook['chatsList']; + chatsListLoading: UseBotChatsListHook['loading']; + getChatsList: UseBotChatsListHook['getChatsList'] } type UseAiBotArgs = { threadId?: string; - prevThreadId?: string; - onChatLoadingError?: () => void; + orgId?: number } export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { - const { threadId, onChatLoadingError } = args; + const { threadId, orgId } = args; const { showMessage, closeSnackbar } = useAlertSnackbar(); let location = useLocation<{skipReloading?: boolean}>(); + const { + chatsList, + loading: chatsListLoading, + getChatsList, + } = useBotChatsList(orgId); + const [messages, setMessages] = useState(null); const [isLoading, setLoading] = useState(false); const [error, setError] = useState(null); const [wsLoading, setWsLoading] = useState(false); const [isChangeVisibilityLoading, setIsChangeVisibilityLoading] = useState(false) - + const token = localStorage.getAuthToken() const onWebSocketError = (error: WebSocketEventMap['error']) => { @@ -92,6 +101,9 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { }); } } + } else if (threadId !== messageData.thread_id) { + const threadInList = chatsList?.find((item) => item.thread_id === messageData.thread_id) + if (!threadInList) getChatsList() } } else { showMessage('An error occurred. Please try again') @@ -137,7 +149,6 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { if (response && response.length > 0) { setMessages(response); } else { - if (onChatLoadingError) onChatLoadingError(); setError({ code: 404, message: 'Specified chat not found or you have no access.', @@ -168,7 +179,6 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { } else if (threadId) { subscribe(threadId) } - return () => { isCancelled = true; }; @@ -294,7 +304,10 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { sendMessage, clearChat, messages, - unsubscribe + unsubscribe, + chatsList, + chatsListLoading, + getChatsList } } @@ -302,7 +315,7 @@ type UseBotChatsListHook = { chatsList: BotMessage[] | null; error: Response | null; loading: boolean; - getChatsList: () => void + getChatsList: () => void; }; export const useBotChatsList = (orgId?: number): UseBotChatsListHook => { diff --git a/ui/packages/platform/src/pages/Bot/index.tsx b/ui/packages/platform/src/pages/Bot/index.tsx index b9ee6c7f..e7c9199e 100644 --- a/ui/packages/platform/src/pages/Bot/index.tsx +++ b/ui/packages/platform/src/pages/Bot/index.tsx @@ -103,11 +103,14 @@ export const BotPage = (props: BotPageProps) => { wsReadyState, isChangeVisibilityLoading, changeChatVisibility, - unsubscribe + unsubscribe, + chatsListLoading, + getChatsList, + chatsList } = useAiBot({ threadId: match.params.threadId, + orgId: orgData.id }); - const {chatsList, loading: chatsListLoading, getChatsList} = useBotChatsList(orgData.id); const matches = useMediaQuery(theme.breakpoints.down('sm')); From 6de888c01b533e0d6d05854a00aa4aabdf7231d3 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Mon, 10 Jun 2024 21:42:13 +0000 Subject: [PATCH 030/111] Bot UI: highlighted show/hide code block string --- .../pages/Bot/Messages/Message/CodeBlock.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx index 47bfb054..61b0d923 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx @@ -6,6 +6,7 @@ import languages from 'react-syntax-highlighter/dist/esm/languages/prism/support import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined'; import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'; +import CodeIcon from '@material-ui/icons/Code'; const useStyles = makeStyles((theme) => ({ container: { @@ -38,18 +39,27 @@ const useStyles = makeStyles((theme) => ({ width: '0.813rem', }, summary: { + color: 'rgb(166, 38, 164)', textDecoration: 'underline', textDecorationStyle: 'dotted', cursor: 'pointer', backgroundColor: 'transparent', boxShadow: 'none', display: 'inline-flex', - minHeight: '32px!important', + minHeight: '2rem!important', padding: 0, '&:hover': { textDecoration: 'none' } }, + summaryText: { + display: 'inline-flex', + alignItems: 'center' + }, + summaryTextIcon: { + marginRight: 8, + fontSize: '1rem' + }, details: { padding: 0, backgroundColor: 'transparent' @@ -117,7 +127,12 @@ export const CodeBlock = memo(({ value, language }: CodeBlockProps) => { return ( } className={classes.summary}> - {expanded ? 'Hide' : 'Show'} code block ({codeLines.length} LOC) + + + {expanded ? 'Hide' : 'Show'} code block ({codeLines.length} LOC) +
From 89ddffadaad36b3fea608b363050ed2e0675929c Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 14 Jun 2024 05:09:02 +0000 Subject: [PATCH 031/111] Bot UI: Display debug messages --- .../platform/src/api/bot/getDebugMessages.ts | 39 ++++++ .../platform/src/pages/Bot/BotWrapper.tsx | 5 +- .../src/pages/Bot/ChatsList/ChatsList.tsx | 13 +- .../src/pages/Bot/Command/Command.tsx | 39 ++++-- .../pages/Bot/DebugConsole/DebugConsole.tsx | 107 +++++++++++++++ .../src/pages/Bot/DebugDialog/DebugDialog.tsx | 112 +++++++++------- .../src/pages/Bot/DebugLogs/DebugLogs.tsx | 31 +++++ .../pages/Bot/HeaderButtons/HeaderButtons.tsx | 26 ++-- .../pages/Bot/Messages/Message/Message.tsx | 24 +++- .../src/pages/Bot/Messages/Messages.tsx | 25 ++-- .../Bot/PublicChatDialog/PublicChatDialog.tsx | 16 +-- .../SettingsPanel.tsx} | 28 +++- .../src/pages/Bot/{hooks.ts => hooks.tsx} | 126 ++++++++++++++---- ui/packages/platform/src/pages/Bot/index.tsx | 76 +++++------ ui/packages/platform/src/pages/Bot/utils.ts | 3 +- .../platform/src/types/api/entities/bot.ts | 13 ++ 16 files changed, 486 insertions(+), 197 deletions(-) create mode 100644 ui/packages/platform/src/api/bot/getDebugMessages.ts create mode 100644 ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx create mode 100644 ui/packages/platform/src/pages/Bot/DebugLogs/DebugLogs.tsx rename ui/packages/platform/src/pages/Bot/{SettingsWithLabel/SettingsWithLabel.tsx => SettingsPanel/SettingsPanel.tsx} (72%) rename ui/packages/platform/src/pages/Bot/{hooks.ts => hooks.tsx} (70%) diff --git a/ui/packages/platform/src/api/bot/getDebugMessages.ts b/ui/packages/platform/src/api/bot/getDebugMessages.ts new file mode 100644 index 00000000..8d566312 --- /dev/null +++ b/ui/packages/platform/src/api/bot/getDebugMessages.ts @@ -0,0 +1,39 @@ +import {request} from "../../helpers/request"; +import { DebugMessage } from "../../types/api/entities/bot"; + +type Req = + | { thread_id: string; message_id?: string } + | { thread_id?: string; message_id: string }; + +export const getDebugMessages = async (req: Req): Promise<{ response: DebugMessage[] | null; error: Response | null }> => { + const { thread_id, message_id } = req; + + const params: { [key: string]: string } = {}; + + if (thread_id) { + params['chat_thread_id'] = `eq.${thread_id}`; + } + + if (message_id) { + params['chat_msg_id'] = `eq.${message_id}`; + } + + const queryString = new URLSearchParams(params).toString(); + + const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; + + try { + const response = await request(`${apiServer}/chat_debug_messages?${queryString}`); + + if (!response.ok) { + return { response: null, error: response }; + } + + const responseData: DebugMessage[] = await response.json(); + + return { response: responseData, error: null }; + + } catch (error) { + return { response: null, error: error as Response }; + } +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/BotWrapper.tsx b/ui/packages/platform/src/pages/Bot/BotWrapper.tsx index a8fe7dc0..d8676010 100644 --- a/ui/packages/platform/src/pages/Bot/BotWrapper.tsx +++ b/ui/packages/platform/src/pages/Bot/BotWrapper.tsx @@ -1,6 +1,7 @@ import { BotPage } from "./index"; import {RouteComponentProps} from "react-router"; import {AlertSnackbarProvider} from "@postgres.ai/shared/components/AlertSnackbar/useAlertSnackbar"; +import { AiBotProvider } from "./hooks"; export interface BotWrapperProps { envData: { @@ -25,7 +26,9 @@ export interface BotWrapperProps { export const BotWrapper = (props: BotWrapperProps) => { return ( - + + + ) } diff --git a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx index a199c3d5..f46c58f9 100644 --- a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx +++ b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx @@ -30,7 +30,7 @@ const useStyles = makeStyles((theme) => ({ [theme.breakpoints.down('sm')]: { height: '100vh!important', marginTop: '0!important', - width: 260, + width: 300, zIndex: 9999 }, '& > ul': { @@ -94,7 +94,6 @@ type ChatsListProps = { loading: boolean; chatsList: BotMessage[] | null; onLinkClick?: (targetThreadId: string) => void; - permalinkId?: string } & HeaderButtonsProps export const ChatsList = (props: ChatsListProps) => { @@ -104,11 +103,10 @@ export const ChatsList = (props: ChatsListProps) => { onClose, chatsList, loading, - currentVisibility, withChatVisibilityButton, - onChatVisibilityClick, + onSettingsClick, onLinkClick, - permalinkId + onConsoleClick } = props; const classes = useStyles(props); const params = useParams<{ org?: string, threadId?: string }>(); @@ -150,10 +148,9 @@ export const ChatsList = (props: ChatsListProps) => { onClose={onClose} onCreateNewChat={onCreateNewChat} isOpen={isOpen} - currentVisibility={currentVisibility} withChatVisibilityButton={withChatVisibilityButton} - onChatVisibilityClick={onChatVisibilityClick} - permalinkId={permalinkId} + onSettingsClick={onSettingsClick} + onConsoleClick={onConsoleClick} /> diff --git a/ui/packages/platform/src/pages/Bot/Command/Command.tsx b/ui/packages/platform/src/pages/Bot/Command/Command.tsx index 8ea86b61..1652350b 100644 --- a/ui/packages/platform/src/pages/Bot/Command/Command.tsx +++ b/ui/packages/platform/src/pages/Bot/Command/Command.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react' +import React, { useState, useRef, useEffect, useMemo } from 'react' import { makeStyles } from '@material-ui/core' import SendRoundedIcon from '@material-ui/icons/SendRounded'; import IconButton from "@material-ui/core/IconButton"; @@ -14,12 +14,13 @@ import { useBuffer } from './useBuffer' import { useCaret } from './useCaret' import { theme } from "@postgres.ai/shared/styles/theme"; import { isMobileDevice } from "../../../utils/utils"; +import { useAiBot } from "../hooks"; +import { ReadyState } from "react-use-websocket"; type Props = { - sendDisabled: boolean - onSend: (value: string) => void threadId?: string + orgId: number | null } @@ -74,10 +75,22 @@ const useStyles = makeStyles((theme) => ( ) export const Command = React.memo((props: Props) => { - const { sendDisabled, onSend, threadId } = props + const { threadId, orgId } = props const classes = useStyles() const isMobile = isMobileDevice(); + + const { + error, + wsReadyState, + wsLoading, + loading, + sendMessage, + chatVisibility + } = useAiBot(); + + const sendDisabled = error !== null || loading || wsLoading || wsReadyState !== ReadyState.OPEN; + // Handle value. const [value, setValue] = useState('') @@ -90,10 +103,19 @@ export const Command = React.memo((props: Props) => { // Input caret. const caret = useCaret(inputRef) - const triggerSend = () => { + const onSend = async (message: string) => { + await sendMessage({ + content: message, + thread_id: threadId || null, + org_id: orgId, + is_public: chatVisibility === 'public' + }) + } + + const triggerSend = async () => { if (!value.trim().length || sendDisabled) return - onSend(value) + await onSend(value) buffer.addNew() setValue(buffer.getCurrent()) } @@ -122,13 +144,13 @@ export const Command = React.memo((props: Props) => { } } - const handleKeyDown = (e: React.KeyboardEvent) => { + const handleKeyDown = async (e: React.KeyboardEvent) => { if (!inputRef.current) return // Trigger to send. if (checkIsSendCmd(e.nativeEvent)) { e.preventDefault() - triggerSend() + await triggerSend() return } @@ -171,7 +193,6 @@ export const Command = React.memo((props: Props) => { setValue('') }, [threadId]); - return (
({ + dialogStyle: { + top: '5%!important', + right: '10%!important', + left: 'unset!important', + height: 'fit-content', + width: 'fit-content', + }, + paper: { + width: '80vw', + height: '70vh', + opacity: '.5', + transition: '.2s ease', + '&:hover': { + opacity: 1 + } + }, + dialogTitle: { + padding: '0.5rem', + '& h2': { + fontSize: '1rem', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center' + } + }, + dialogContent: { + padding: 0, + margin: 0, + } + })) + +type DebugConsoleProps = { + isOpen: boolean + onClose: () => void + threadId?: string +} + +export const DebugConsole = (props: DebugConsoleProps) => { + const { isOpen, onClose, threadId } = props; + const { debugMessages, debugMessagesLoading } = useAiBot(); + const classes = useStyles(); + const containerRef = useRef(null); + + useEffect(() => { + if (containerRef.current && debugMessages?.length && threadId && !debugMessagesLoading && isOpen) { + let code: HTMLElement = containerRef.current.getElementsByTagName('code')?.[0]; + if (code.hasChildNodes()) { + code.appendChild(document.createTextNode(`[${debugMessages[debugMessages.length - 1].created_at}]: ${debugMessages[debugMessages.length - 1].content}\n`)) + } else { + debugMessages.forEach((item) => { + code.appendChild(document.createTextNode(`[${item.created_at}]: ${item.content}\n`)) + }) + const container = document.getElementById(`logs-container-${threadId}`); + if (container) { + container.appendChild(code) + } + } + } + }, [debugMessages, isOpen, threadId, debugMessagesLoading]); + + return ( + + + Debug console + + + + + + + + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx b/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx index a752214d..4edc464a 100644 --- a/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx +++ b/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect, useRef, useState } from 'react' import Dialog from "@material-ui/core/Dialog"; import DialogTitle from '@material-ui/core/DialogTitle'; import { DialogContent, IconButton, makeStyles, Typography } from "@material-ui/core"; @@ -6,11 +6,18 @@ import ReactMarkdown from "react-markdown"; import Format from "../../../utils/format"; import { icons } from "@postgres.ai/shared/styles/icons"; import { disallowedHtmlTagsForMarkdown } from "../utils"; +import { BotMessage, DebugMessage } from "../../../types/api/entities/bot"; +import rehypeRaw from "rehype-raw"; +import remarkGfm from "remark-gfm"; +import { getDebugMessages } from "../../../api/bot/getDebugMessages"; +import { PageSpinner } from "@postgres.ai/shared/components/PageSpinner"; +import { SyntaxHighlight } from "@postgres.ai/shared/components/SyntaxHighlight"; +import { DebugLogs } from "../DebugLogs/DebugLogs"; type DebugDialogProps = { isOpen: boolean; onClose: () => void; - debugMessages: { created_at: string, message: string }[] + messageId: string } const useStyles = makeStyles( @@ -28,30 +35,60 @@ const useStyles = makeStyles( top: theme.spacing(1), color: theme.palette.grey[500], }, - message: { - marginBottom: 12, - '& > *': { - fontFamily: "'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New'," + - " 'andale mono', 'lucida console', monospace", - fontSize: '0.813rem', - lineHeight: '120%' - }, - }, - time: { - marginBottom: 4, - display: 'inline-block' + dialogContent: { + backgroundColor: 'rgb(250, 250, 250)', + padding: '0.5rem 0', + '& pre': { + whiteSpace: 'pre-wrap!important' + } } })) export const DebugDialog = (props: DebugDialogProps) => { - const {isOpen, onClose, debugMessages} = props; + const {isOpen, onClose, messageId} = props; const classes = useStyles() + + const [debugLoading, setDebugLoading] = useState(false); + const debugMessages = useRef(null) + + const generateMessages = (messages: DebugMessage[]) => { + const code = document.createElement('code'); + messages.forEach((item) => { + code.appendChild(document.createTextNode(`[${item.created_at}]: ${item.content}\n`)) + }) + const container = document.getElementById(`logs-container-${messageId}`); + if (container) { + container.appendChild(code) + } + } + + const getDebugMessagesForMessage = async () => { + setDebugLoading(true) + if (messageId) { + const { response} = await getDebugMessages({ message_id: messageId }) + if (response) { + debugMessages.current = response; + generateMessages(response) + } + } + } + + useEffect(() => { + if (isOpen && !debugMessages.current) { + getDebugMessagesForMessage() + .then(() => setDebugLoading(false)) + } else if (isOpen && debugMessages.current && debugMessages.current?.length > 0) { + setTimeout(() => generateMessages(debugMessages.current!), 0) + } + }, [isOpen]); + + return ( Debug @@ -63,41 +100,14 @@ export const DebugDialog = (props: DebugDialogProps) => { {icons.closeIcon} - - { - debugMessages.map((debugMessage) => { - const formattedTime = debugMessage.created_at ? Format.timeAgo(debugMessage.created_at) : null - return ( -
- - {formattedTime} - - - {debugMessage.message} - -
- ) - } - ) - } - { - (!debugMessages || debugMessages.length === 0) && ( -
- No debug information available for the selected message. -
- ) - } + +
) diff --git a/ui/packages/platform/src/pages/Bot/DebugLogs/DebugLogs.tsx b/ui/packages/platform/src/pages/Bot/DebugLogs/DebugLogs.tsx new file mode 100644 index 00000000..d556235a --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/DebugLogs/DebugLogs.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { SyntaxHighlight } from "@postgres.ai/shared/components/SyntaxHighlight"; + +type DebugLogsProps = { + isLoading: boolean + isEmpty: boolean + id: string +} + +export const DebugLogs = (props: DebugLogsProps) => { + const { isLoading, isEmpty, id } = props; + return ( + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/HeaderButtons/HeaderButtons.tsx b/ui/packages/platform/src/pages/Bot/HeaderButtons/HeaderButtons.tsx index 1c4ab51d..5577219a 100644 --- a/ui/packages/platform/src/pages/Bot/HeaderButtons/HeaderButtons.tsx +++ b/ui/packages/platform/src/pages/Bot/HeaderButtons/HeaderButtons.tsx @@ -1,12 +1,12 @@ import React from "react"; +import { Button, makeStyles, useMediaQuery } from "@material-ui/core"; import IconButton from "@material-ui/core/IconButton"; import NavigateBeforeIcon from "@material-ui/icons/NavigateBefore"; import NavigateNextIcon from '@material-ui/icons/NavigateNext'; -import { Button, makeStyles, useMediaQuery } from "@material-ui/core"; import AddCircleOutlineIcon from "@material-ui/icons/AddCircleOutline"; import Box from "@mui/material/Box"; import { theme } from "@postgres.ai/shared/styles/theme"; -import { SettingsWithLabel } from "../SettingsWithLabel/SettingsWithLabel"; +import { SettingsPanel, SettingsPanelProps } from "../SettingsPanel/SettingsPanel"; export type HeaderButtonsProps = { @@ -14,9 +14,8 @@ export type HeaderButtonsProps = { onClose: () => void; onCreateNewChat: () => void; withChatVisibilityButton: boolean; - onChatVisibilityClick?: () => void; - currentVisibility: 'public' | 'private'; - permalinkId?: string; + onSettingsClick: SettingsPanelProps["onSettingsClick"]; + onConsoleClick: SettingsPanelProps["onConsoleClick"]; } const useStyles = makeStyles((theme) => ({ @@ -58,23 +57,22 @@ export const HeaderButtons = (props: HeaderButtonsProps) => { onClose, onCreateNewChat, isOpen, - onChatVisibilityClick, + onSettingsClick, withChatVisibilityButton, - currentVisibility, - permalinkId + onConsoleClick } = props; const matches = useMediaQuery(theme.breakpoints.down('sm')); const classes = useStyles(); + return ( { - withChatVisibilityButton && onChatVisibilityClick && - + withChatVisibilityButton && + } + ) } \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/hooks.ts b/ui/packages/platform/src/pages/Bot/hooks.tsx similarity index 70% rename from ui/packages/platform/src/pages/Bot/hooks.ts rename to ui/packages/platform/src/pages/Bot/hooks.tsx index bc3846ca..815c50fd 100644 --- a/ui/packages/platform/src/pages/Bot/hooks.ts +++ b/ui/packages/platform/src/pages/Bot/hooks.tsx @@ -5,16 +5,17 @@ *-------------------------------------------------------------------------- */ -import {useCallback, useEffect, useRef, useState} from "react"; +import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; import useWebSocket, {ReadyState} from "react-use-websocket"; import { useLocation } from "react-router-dom"; -import {BotMessage} from "../../types/api/entities/bot"; +import { BotMessage, DebugMessage } from "../../types/api/entities/bot"; import {getChatsWithWholeThreads} from "../../api/bot/getChatsWithWholeThreads"; import {getChats} from "api/bot/getChats"; import {useAlertSnackbar} from "@postgres.ai/shared/components/AlertSnackbar/useAlertSnackbar"; import {localStorage} from "../../helpers/localStorage"; import { makeChatPublic } from "../../api/bot/makeChatPublic"; -import { usePrev } from "../../hooks/usePrev"; +import { getDebugMessages } from "../../api/bot/getDebugMessages"; +import { messages } from "../../assets/messages"; const WS_URL = process.env.REACT_APP_WS_URL || ''; @@ -42,10 +43,14 @@ type UseAiBotReturnType = { wsReadyState: ReadyState; changeChatVisibility: (threadId: string, isPublic: boolean) => void; isChangeVisibilityLoading: boolean; - unsubscribe: (threadId: string) => void; + unsubscribe: (threadId: string) => void + chatVisibility: 'public' | 'private' + debugMessages: DebugMessage[] | null + getDebugMessagesForWholeThread: () => void chatsList: UseBotChatsListHook['chatsList']; chatsListLoading: UseBotChatsListHook['loading']; - getChatsList: UseBotChatsListHook['getChatsList'] + getChatsList: UseBotChatsListHook['getChatsList']; + debugMessagesLoading: boolean; } type UseAiBotArgs = { @@ -53,7 +58,7 @@ type UseAiBotArgs = { orgId?: number } -export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { +export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => { const { threadId, orgId } = args; const { showMessage, closeSnackbar } = useAlertSnackbar(); let location = useLocation<{skipReloading?: boolean}>(); @@ -65,11 +70,15 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { } = useBotChatsList(orgId); const [messages, setMessages] = useState(null); + const [debugMessages, setDebugMessages] = useState(null); + const [debugMessagesLoading, setDebugMessagesLoading] = useState(false); const [isLoading, setLoading] = useState(false); const [error, setError] = useState(null); const [wsLoading, setWsLoading] = useState(false); - const [isChangeVisibilityLoading, setIsChangeVisibilityLoading] = useState(false) + const [chatVisibility, setChatVisibility] = useState('public'); + const [isChangeVisibilityLoading, setIsChangeVisibilityLoading] = useState(false); + const token = localStorage.getAuthToken() const onWebSocketError = (error: WebSocketEventMap['error']) => { @@ -79,31 +88,42 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { const onWebSocketMessage = (event: WebSocketEventMap['message']) => { if (event.data) { - const messageData: BotMessage = JSON.parse(event.data); + const messageData: BotMessage | DebugMessage = JSON.parse(event.data); if (messageData) { - if ((threadId && threadId === messageData.thread_id) || (!threadId && messageData.parent_id && messages)) { - let currentMessages = [...(messages || [])]; - // Check if the last message needs its data updated - const lastMessage = currentMessages[currentMessages.length - 1]; - if (lastMessage && !lastMessage.id && messageData.parent_id) { - lastMessage.id = messageData.parent_id; - lastMessage.created_at = messageData.created_at; - lastMessage.is_public = messageData.is_public; - } + const isThreadMatching = threadId && threadId === messageData.thread_id; + const isParentMatching = !threadId && 'parent_id' in messageData && messageData.parent_id && messages; + const isDebugMessage = messageData.type === 'debug'; + if (isThreadMatching || isParentMatching || isDebugMessage) { + if (isDebugMessage) { + let currentDebugMessages = [...(debugMessages || [])]; + currentDebugMessages.push(messageData) + setDebugMessages(currentDebugMessages) + } else { + // Check if the last message needs its data updated + let currentMessages = [...(messages || [])]; + const lastMessage = currentMessages[currentMessages.length - 1]; + if (lastMessage && !lastMessage.id && messageData.parent_id) { + lastMessage.id = messageData.parent_id; + lastMessage.created_at = messageData.created_at; + lastMessage.is_public = messageData.is_public; + } - currentMessages.push(messageData); - setMessages(currentMessages); - if (document.visibilityState === "hidden") { - if (Notification.permission === "granted") { - new Notification("New message", { - body: 'New message from Postgres.AI Bot', - icon: '/images/bot_avatar.png' - }); + currentMessages.push(messageData); + setMessages(currentMessages); + setWsLoading(false); + if (document.visibilityState === "hidden") { + if (Notification.permission === "granted") { + new Notification("New message", { + body: 'New message from Postgres.AI Bot', + icon: '/images/bot_avatar.png' + }); + } } } } else if (threadId !== messageData.thread_id) { const threadInList = chatsList?.find((item) => item.thread_id === messageData.thread_id) if (!threadInList) getChatsList() + setWsLoading(false); } } else { showMessage('An error occurred. Please try again') @@ -111,7 +131,7 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { } else { showMessage('An error occurred. Please try again') } - setWsLoading(false); + setLoading(false); } @@ -141,10 +161,11 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { const getChatMessages = useCallback(async (threadId: string) => { setError(null); + setDebugMessages(null) if (threadId) { setLoading(true); try { - const { response} = await getChatsWithWholeThreads({id: threadId}); + const { response } = await getChatsWithWholeThreads({id: threadId}); subscribe(threadId) if (response && response.length > 0) { setMessages(response); @@ -239,6 +260,8 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { const clearChat = () => { setMessages(null); + setDebugMessages(null); + setWsLoading(false); } const changeChatVisibility = async (threadId: string, isPublic: boolean) => { @@ -282,6 +305,17 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { })) } + const getDebugMessagesForWholeThread = async () => { + setDebugMessagesLoading(true) + if (threadId) { + const { response } = await getDebugMessages({thread_id: threadId}) + if (response) { + setDebugMessages(response) + } + } + setDebugMessagesLoading(false) + } + useEffect(() => { if ('Notification' in window) { Notification.requestPermission().then(permission => { @@ -294,6 +328,12 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { } }, []) + useEffect(() => { + if (messages && messages.length > 0 && threadId) { + setChatVisibility(messages[0].is_public ? 'public' : 'private') + } + }, [messages]); + return { error: error, wsLoading: wsLoading, @@ -304,13 +344,43 @@ export const useAiBot = (args: UseAiBotArgs): UseAiBotReturnType => { sendMessage, clearChat, messages, + getDebugMessagesForWholeThread, unsubscribe, chatsList, chatsListLoading, - getChatsList + getChatsList, + chatVisibility, + debugMessages, + debugMessagesLoading } } +type AiBotContextType = UseAiBotReturnType; + +const AiBotContext = createContext(undefined); + +type AiBotProviderProps = { + children: React.ReactNode; + args: UseAiBotArgs; +}; + +export const AiBotProvider = ({ children, args }: AiBotProviderProps) => { + const aiBot = useAiBotProviderValue(args); + return ( + + {children} + + ); +}; + +export const useAiBot = (): AiBotContextType => { + const context = useContext(AiBotContext); + if (context === undefined) { + throw new Error('useAiBotContext must be used within an AiBotProvider'); + } + return context; +}; + type UseBotChatsListHook = { chatsList: BotMessage[] | null; error: Response | null; diff --git a/ui/packages/platform/src/pages/Bot/index.tsx b/ui/packages/platform/src/pages/Bot/index.tsx index e7c9199e..5ef1a062 100644 --- a/ui/packages/platform/src/pages/Bot/index.tsx +++ b/ui/packages/platform/src/pages/Bot/index.tsx @@ -7,7 +7,6 @@ import React, { useEffect, useState } from 'react'; import cn from "classnames"; -import {ReadyState} from "react-use-websocket"; import Box from '@mui/material/Box/Box'; import { makeStyles, useMediaQuery } from "@material-ui/core"; import { useHistory, useRouteMatch } from "react-router-dom"; @@ -17,14 +16,15 @@ import { Messages } from './Messages/Messages'; import { Command } from './Command/Command'; import { ChatsList } from "./ChatsList/ChatsList"; import { BotWrapperProps } from "./BotWrapper"; -import { useBotChatsList, useAiBot } from "./hooks"; +import { useAiBot } from "./hooks"; import { usePrev } from "../../hooks/usePrev"; import {HeaderButtons} from "./HeaderButtons/HeaderButtons"; import settings from "../../utils/settings"; import { PublicChatDialog } from "./PublicChatDialog/PublicChatDialog"; import { theme } from "@postgres.ai/shared/styles/theme"; import { colors } from "@postgres.ai/shared/styles/colors"; -import { SettingsWithLabel } from "./SettingsWithLabel/SettingsWithLabel"; +import { SettingsPanel } from "./SettingsPanel/SettingsPanel"; +import { DebugConsole } from "./DebugConsole/DebugConsole"; type BotPageProps = BotWrapperProps; @@ -43,7 +43,7 @@ const useStyles = makeStyles( width: 192, marginLeft: 52, [theme.breakpoints.down('sm')]: { - width: 226 + width: 266 } }, toggleListButton: { @@ -95,28 +95,22 @@ export const BotPage = (props: BotPageProps) => { const { messages, - loading, error, - sendMessage, clearChat, - wsLoading, - wsReadyState, isChangeVisibilityLoading, changeChatVisibility, unsubscribe, + getDebugMessagesForWholeThread, chatsListLoading, getChatsList, chatsList - } = useAiBot({ - threadId: match.params.threadId, - orgId: orgData.id - }); + } = useAiBot(); const matches = useMediaQuery(theme.breakpoints.down('sm')); const [isChatsListVisible, setChatsListVisible] = useState(window?.innerWidth > 640); const [isVisibilityDialogVisible, setVisibilityDialogVisible] = useState(false); - const [chatVisibility, setChatVisibility] = useState<'public' | 'private'>('public'); + const [isDebugConsoleVisible, setDebugConsoleVisible] = useState(false); const history = useHistory(); @@ -144,16 +138,13 @@ export const BotPage = (props: BotPageProps) => { setVisibilityDialogVisible((prevState) => !prevState) } - const handleSendMessage = async (message: string) => { - const { threadId } = match.params; - const orgId = orgData.id || null; + const handleOpenDebugConsole = () => { + setDebugConsoleVisible(true); + getDebugMessagesForWholeThread() + } - await sendMessage({ - content: message, - thread_id: threadId || null, - org_id: orgId, - is_public: chatVisibility === 'public' - }) + const toggleDebugConsole = () => { + setDebugConsoleVisible((prevState) => !prevState) } const handleCreateNewChat = () => { @@ -184,17 +175,15 @@ export const BotPage = (props: BotPageProps) => { } }, [match.params.threadId, match.params.org, messages, prevThreadId]); - useEffect(() => { - if (messages && messages.length > 0 && match.params.threadId) { - setChatVisibility(messages[0].is_public ? 'public' : 'private') - } - }, [messages]); - useEffect(() => { // fixes hack with skipping additional loading chats_ancestors_and_descendants history.replace({ state: {} }) }, []); + useEffect(() => { + if (isDebugConsoleVisible) setDebugConsoleVisible(false) + }, [match.params.threadId]); + if (error && error.code === 404) { return ( <> @@ -209,8 +198,12 @@ export const BotPage = (props: BotPageProps) => { return ( <> + {match.params.threadId && { chatsList={chatsList} loading={chatsListLoading} withChatVisibilityButton={matches && Boolean(match.params.threadId)} - onChatVisibilityClick={toggleVisibilityDialog} - currentVisibility={chatVisibility} + onSettingsClick={toggleVisibilityDialog} onLinkClick={handleChatListLinkClick} - permalinkId={messages?.[0]?.id} + onConsoleClick={handleOpenDebugConsole} /> {match.params.threadId && !matches && - } { onClose={toggleChatsList} onCreateNewChat={handleCreateNewChat} withChatVisibilityButton={matches && Boolean(match.params.threadId)} - onChatVisibilityClick={toggleVisibilityDialog} - currentVisibility={chatVisibility} - permalinkId={messages?.[0]?.id} + onSettingsClick={toggleVisibilityDialog} + onConsoleClick={handleOpenDebugConsole} /> - - + diff --git a/ui/packages/platform/src/pages/Bot/utils.ts b/ui/packages/platform/src/pages/Bot/utils.ts index ecf04052..be1f8c86 100644 --- a/ui/packages/platform/src/pages/Bot/utils.ts +++ b/ui/packages/platform/src/pages/Bot/utils.ts @@ -6,6 +6,7 @@ */ import { API_URL_PREFIX } from "../../config/env"; +import { BotMessage, BotMessageWithDebugInfo, DebugMessage } from "../../types/api/entities/bot"; export const permalinkLinkBuilder = (id: string): string => { const apiUrl = process.env.REACT_APP_API_URL_PREFIX || API_URL_PREFIX; @@ -29,4 +30,4 @@ export const disallowedHtmlTagsForMarkdown= [ 'frameset', 'audio', 'video', -] \ No newline at end of file +]; \ No newline at end of file diff --git a/ui/packages/platform/src/types/api/entities/bot.ts b/ui/packages/platform/src/types/api/entities/bot.ts index be26e1d0..3ceb963a 100644 --- a/ui/packages/platform/src/types/api/entities/bot.ts +++ b/ui/packages/platform/src/types/api/entities/bot.ts @@ -1,3 +1,11 @@ +export type DebugMessage = { + type: 'debug' + message_id: string | null + org_id: string + thread_id: string + content: string + created_at: string +} export type BotMessage = { id: string @@ -13,4 +21,9 @@ export type BotMessage = { user_id: string org_id: string thread_id: string + type: 'message' | undefined +} + +export type BotMessageWithDebugInfo = BotMessage & { + debugMessages?: DebugMessage[] } \ No newline at end of file From 94565dcd769eb67ab303e68355cec1a445f116da Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Mon, 17 Jun 2024 16:27:24 +0000 Subject: [PATCH 032/111] Add K8s Agent config --- .gitlab/agents/k8s-cluster-1/config.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitlab/agents/k8s-cluster-1/config.yaml diff --git a/.gitlab/agents/k8s-cluster-1/config.yaml b/.gitlab/agents/k8s-cluster-1/config.yaml new file mode 100644 index 00000000..e35cc7b8 --- /dev/null +++ b/.gitlab/agents/k8s-cluster-1/config.yaml @@ -0,0 +1,3 @@ +ci_access: + projects: + - id: 14976868 From a2acf9f542bc3dfe51076dfd00fb048bded4dcc4 Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Mon, 17 Jun 2024 21:06:00 +0000 Subject: [PATCH 033/111] k8s agent context --- .gitlab/agents/k8s-cluster-1/config.yaml | 2 +- ui/packages/platform/.gitlab-ci.yml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitlab/agents/k8s-cluster-1/config.yaml b/.gitlab/agents/k8s-cluster-1/config.yaml index e35cc7b8..73481f44 100644 --- a/.gitlab/agents/k8s-cluster-1/config.yaml +++ b/.gitlab/agents/k8s-cluster-1/config.yaml @@ -1,3 +1,3 @@ ci_access: projects: - - id: 14976868 + - id: postgres-ai/database-lab diff --git a/ui/packages/platform/.gitlab-ci.yml b/ui/packages/platform/.gitlab-ci.yml index fc8c5776..79ca948e 100644 --- a/ui/packages/platform/.gitlab-ci.yml +++ b/ui/packages/platform/.gitlab-ci.yml @@ -86,6 +86,9 @@ script: # Substitute env variables in deploy config. - bash ./ui/packages/platform/do.sh subs_envs ./ui/packages/platform/deploy/platform-console.yaml /tmp/platform-console.yaml + # Context + - kubectl config get-contexts + - kubectl config use-context postgres-ai/database-lab:k8s-cluster-1 # Deploy to k8s cluster. - kubectl apply --filename /tmp/platform-console.yaml -n $NAMESPACE From d47e61752f17b2be36703bd490828800cea54be1 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 21 Jun 2024 13:37:34 +0000 Subject: [PATCH 034/111] Bot UI: Make links clickable in debug messages, fixed webkit behavior --- .../pages/Bot/DebugConsole/DebugConsole.tsx | 34 +++++++++---------- .../src/pages/Bot/DebugDialog/DebugDialog.tsx | 27 +++++++-------- ui/packages/platform/src/pages/Bot/utils.ts | 29 +++++++++++++++- 3 files changed, 56 insertions(+), 34 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx b/ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx index 05e6ff44..a0ab2db9 100644 --- a/ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx +++ b/ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx @@ -1,15 +1,11 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef } from "react"; import Dialog from "@material-ui/core/Dialog"; import { DialogContent, DialogTitle, makeStyles } from "@material-ui/core"; import { useAiBot } from "../hooks"; -import rehypeRaw from "rehype-raw"; -import remarkGfm from "remark-gfm"; -import ReactMarkdown from "react-markdown"; import IconButton from "@material-ui/core/IconButton"; import CloseIcon from "@material-ui/icons/Close"; -import { disallowedHtmlTagsForMarkdown } from "../utils"; import { DebugLogs } from "../DebugLogs/DebugLogs"; -import { DebugMessage } from "../../../types/api/entities/bot"; +import { createMessageFragment } from "../utils"; const useStyles = makeStyles( (theme) => ({ @@ -17,12 +13,12 @@ const useStyles = makeStyles( top: '5%!important', right: '10%!important', left: 'unset!important', - height: 'fit-content', - width: 'fit-content', + height: '80vh', + width: '80vw', }, paper: { width: '80vw', - height: '70vh', + height: '80vh', opacity: '.5', transition: '.2s ease', '&:hover': { @@ -58,17 +54,19 @@ export const DebugConsole = (props: DebugConsoleProps) => { useEffect(() => { if (containerRef.current && debugMessages?.length && threadId && !debugMessagesLoading && isOpen) { - let code: HTMLElement = containerRef.current.getElementsByTagName('code')?.[0]; + let code = containerRef.current.getElementsByTagName('code')?.[0]; + if (!code) { + code = document.createElement('code'); + containerRef.current.appendChild(code); + } + if (code.hasChildNodes()) { - code.appendChild(document.createTextNode(`[${debugMessages[debugMessages.length - 1].created_at}]: ${debugMessages[debugMessages.length - 1].content}\n`)) + const lastMessage = debugMessages[debugMessages.length - 1]; + const fragment = createMessageFragment([lastMessage]); + code.appendChild(fragment); } else { - debugMessages.forEach((item) => { - code.appendChild(document.createTextNode(`[${item.created_at}]: ${item.content}\n`)) - }) - const container = document.getElementById(`logs-container-${threadId}`); - if (container) { - container.appendChild(code) - } + const fragment = createMessageFragment(debugMessages); + code.appendChild(fragment); } } }, [debugMessages, isOpen, threadId, debugMessagesLoading]); diff --git a/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx b/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx index 4edc464a..fb64b723 100644 --- a/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx +++ b/ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx @@ -1,18 +1,12 @@ import React, { useEffect, useRef, useState } from 'react' import Dialog from "@material-ui/core/Dialog"; import DialogTitle from '@material-ui/core/DialogTitle'; -import { DialogContent, IconButton, makeStyles, Typography } from "@material-ui/core"; -import ReactMarkdown from "react-markdown"; -import Format from "../../../utils/format"; +import { DialogContent, IconButton, makeStyles } from "@material-ui/core"; import { icons } from "@postgres.ai/shared/styles/icons"; -import { disallowedHtmlTagsForMarkdown } from "../utils"; -import { BotMessage, DebugMessage } from "../../../types/api/entities/bot"; -import rehypeRaw from "rehype-raw"; -import remarkGfm from "remark-gfm"; +import { DebugMessage } from "../../../types/api/entities/bot"; import { getDebugMessages } from "../../../api/bot/getDebugMessages"; -import { PageSpinner } from "@postgres.ai/shared/components/PageSpinner"; -import { SyntaxHighlight } from "@postgres.ai/shared/components/SyntaxHighlight"; import { DebugLogs } from "../DebugLogs/DebugLogs"; +import { createMessageFragment } from "../utils"; type DebugDialogProps = { isOpen: boolean; @@ -52,15 +46,18 @@ export const DebugDialog = (props: DebugDialogProps) => { const debugMessages = useRef(null) const generateMessages = (messages: DebugMessage[]) => { - const code = document.createElement('code'); - messages.forEach((item) => { - code.appendChild(document.createTextNode(`[${item.created_at}]: ${item.content}\n`)) - }) const container = document.getElementById(`logs-container-${messageId}`); if (container) { - container.appendChild(code) + let code = container.getElementsByTagName('code')?.[0]; + if (!code) { + code = document.createElement('code'); + container.appendChild(code); + } + + const fragment = createMessageFragment(messages); + code.appendChild(fragment); } - } + }; const getDebugMessagesForMessage = async () => { setDebugLoading(true) diff --git a/ui/packages/platform/src/pages/Bot/utils.ts b/ui/packages/platform/src/pages/Bot/utils.ts index be1f8c86..d6b45f20 100644 --- a/ui/packages/platform/src/pages/Bot/utils.ts +++ b/ui/packages/platform/src/pages/Bot/utils.ts @@ -30,4 +30,31 @@ export const disallowedHtmlTagsForMarkdown= [ 'frameset', 'audio', 'video', -]; \ No newline at end of file +]; + +export const createMessageFragment = (messages: DebugMessage[]): DocumentFragment => { + const fragment = document.createDocumentFragment(); + + messages.forEach((item) => { + const textBeforeLink = `[${item.created_at}]: `; + const parts = item.content.split(/(https?:\/\/[^\s]+)/g); + + const messageContent = parts.map((part) => { + if (/https?:\/\/[^\s]+/.test(part)) { + const link = document.createElement('a'); + link.href = part; + link.target = '_blank'; + link.textContent = part; + return link; + } else { + return document.createTextNode(part); + } + }); + + fragment.appendChild(document.createTextNode(textBeforeLink)); + messageContent.forEach((node) => fragment.appendChild(node)); + fragment.appendChild(document.createTextNode('\n')); + }); + + return fragment; +}; \ No newline at end of file From bdb5d1cc8b489ad198c021afd920bcf07bf4d2cd Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 21 Jun 2024 13:50:14 +0000 Subject: [PATCH 035/111] Bot UI: Floating intercom --- ui/packages/platform/src/pages/Bot/Command/Command.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ui/packages/platform/src/pages/Bot/Command/Command.tsx b/ui/packages/platform/src/pages/Bot/Command/Command.tsx index 1652350b..18f8b814 100644 --- a/ui/packages/platform/src/pages/Bot/Command/Command.tsx +++ b/ui/packages/platform/src/pages/Bot/Command/Command.tsx @@ -16,6 +16,7 @@ import { theme } from "@postgres.ai/shared/styles/theme"; import { isMobileDevice } from "../../../utils/utils"; import { useAiBot } from "../hooks"; import { ReadyState } from "react-use-websocket"; +import { useFloatingIntercom } from "../../../hooks/useFloatingIntercom"; type Props = { @@ -193,6 +194,11 @@ export const Command = React.memo((props: Props) => { setValue('') }, [threadId]); + // Floating intercom. + const sendButtonRef = useRef(null) + + useFloatingIntercom(sendButtonRef) + return (
{ onClick={triggerSend} className={classes.iconButton} disabled={sendDisabled || value.length === 0} + ref={sendButtonRef} > From 70e9cd5833010caf51d66d07a64e0a793db2ca75 Mon Sep 17 00:00:00 2001 From: Dmitry Fomin Date: Mon, 8 Jul 2024 16:13:42 +0000 Subject: [PATCH 036/111] update checkup container version and mount path --- ui/packages/platform/src/utils/cfggen.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/packages/platform/src/utils/cfggen.ts b/ui/packages/platform/src/utils/cfggen.ts index d239e1f7..33f9e3af 100644 --- a/ui/packages/platform/src/utils/cfggen.ts +++ b/ui/packages/platform/src/utils/cfggen.ts @@ -153,7 +153,7 @@ export DB_PWD=**** `# Start check health of Postgres databases docker run \\ -v $(pwd)/${data.projectName}.yml:/${data.projectName}.yml \\ - -v $(pwd)/artifacts:/artifacts \\` + -v $(pwd)/artifacts:/checkup/artifacts \\` if (data.sshKeysPath !== '') { result = @@ -169,7 +169,7 @@ docker run \\ -e ${hostsType}="${hosts}" \\ -e CHECKUP_SNAPSHOT_DISTANCE_SECONDS=${data.collectPeriod} \\ -e PGPASSWORD="$\{DB_PWD}" \\ - registry.gitlab.com/postgres-ai/postgres-checkup:1-5-1 bash run_checkup.sh + registry.gitlab.com/postgres-ai/postgres-checkup:1-5-3 bash run_checkup.sh ` return result From 99efca78d5af0df63f070cfcfb180a6c8a3a2c30 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Mon, 8 Jul 2024 16:13:44 +0000 Subject: [PATCH 037/111] Bot UI: allow configuring LLM model --- ui/packages/platform/src/api/api.js | 15 +- .../platform/src/api/bot/getAiModels.ts | 23 +++ ...eChatPublic.ts => updateChatVisibility.ts} | 6 +- .../BotSettingsForm/BotSettingsForm.tsx | 12 +- .../src/pages/Bot/ChatsList/ChatsList.tsx | 13 +- .../pages/Bot/DebugConsole/DebugConsole.tsx | 12 ++ .../pages/Bot/HeaderButtons/HeaderButtons.tsx | 2 +- .../pages/Bot/Messages/Message/Message.tsx | 20 ++- .../src/pages/Bot/Messages/Messages.tsx | 4 +- .../SettingsDialog.tsx} | 132 +++++++++++++----- .../pages/Bot/SettingsPanel/SettingsPanel.tsx | 69 +++++++-- ui/packages/platform/src/pages/Bot/hooks.tsx | 103 ++++++++++++-- ui/packages/platform/src/pages/Bot/index.tsx | 44 ++---- .../platform/src/types/api/entities/bot.ts | 10 +- 14 files changed, 352 insertions(+), 113 deletions(-) create mode 100644 ui/packages/platform/src/api/bot/getAiModels.ts rename ui/packages/platform/src/api/bot/{makeChatPublic.ts => updateChatVisibility.ts} (74%) rename ui/packages/platform/src/pages/Bot/{PublicChatDialog/PublicChatDialog.tsx => SettingsDialog/SettingsDialog.tsx} (65%) diff --git a/ui/packages/platform/src/api/api.js b/ui/packages/platform/src/api/api.js index ce839871..2c575f91 100644 --- a/ui/packages/platform/src/api/api.js +++ b/ui/packages/platform/src/api/api.js @@ -453,20 +453,21 @@ class Api { updateAiBotSettings(token, orgId, orgData) { let params = {}; + let data = {}; let headers = { Authorization: 'Bearer ' + token, prefer: 'return=representation' }; - if (typeof orgData.custom_prompt !== 'undefined') { - params.custom_prompt = orgData.custom_prompt; - } - + // if (typeof orgData.custom_prompt !== 'undefined') { + // data.custom_prompt = orgData.custom_prompt; + // } if (typeof orgData.model !== 'undefined') { - params.model = orgData.model; + data.ai_model = orgData.model; + } + params.data = { + ai_bot: data } - - console.log('params', params) return this.patch(`${this.apiServer}/orgs?id=eq.` + orgId, params, { headers: headers diff --git a/ui/packages/platform/src/api/bot/getAiModels.ts b/ui/packages/platform/src/api/bot/getAiModels.ts new file mode 100644 index 00000000..2e4e2056 --- /dev/null +++ b/ui/packages/platform/src/api/bot/getAiModels.ts @@ -0,0 +1,23 @@ +import {request} from "../../helpers/request"; +import { AiModel } from "../../types/api/entities/bot"; + +export const getAiModels = async (): Promise<{ response: AiModel[] | null; error: Response | null }> => { + const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; + + try { + const response = await request(`${apiServer}/llm_models`, { + method: 'GET', + }); + + if (!response.ok) { + return { response: null, error: response }; + } + + const responseData: AiModel[] = await response.json(); + + return { response: responseData, error: null }; + + } catch (error) { + return { response: null, error: error as Response }; + } +} \ No newline at end of file diff --git a/ui/packages/platform/src/api/bot/makeChatPublic.ts b/ui/packages/platform/src/api/bot/updateChatVisibility.ts similarity index 74% rename from ui/packages/platform/src/api/bot/makeChatPublic.ts rename to ui/packages/platform/src/api/bot/updateChatVisibility.ts index d66e76e5..9a7dd486 100644 --- a/ui/packages/platform/src/api/bot/makeChatPublic.ts +++ b/ui/packages/platform/src/api/bot/updateChatVisibility.ts @@ -6,19 +6,19 @@ type Req = { is_public: boolean } -export const makeChatPublic = async (req: Req): Promise<{ response: BotMessage | null; error: Response | null }> => { +export const updateChatVisibility = async (req: Req): Promise<{ response: BotMessage | null; error: Response | null }> => { const { thread_id, is_public } = req; const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; try { - const response = await request(`${apiServer}/chats_internal?thread_id=eq.${thread_id}`, { + const response = await request(`${apiServer}/chats_auth?thread_id=eq.${thread_id}`, { method: 'PATCH', headers: { Prefer: 'return=representation' }, body: JSON.stringify({ - is_public + is_public, }) }); diff --git a/ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx b/ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx index 730b7175..0bbc0aa8 100644 --- a/ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx +++ b/ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx @@ -52,7 +52,7 @@ interface BotSettingState { updateErrorFields: string[] data: { custom_prompt: string - model: string + ai_model: string } } | null } | null @@ -81,7 +81,7 @@ class BotSettingsForm extends Component } label="gemini-1.5-pro" /> - } label="gpt-4-turbo " /> + } label="gpt-4-turbo " /> } label="Llama 3" /> diff --git a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx index f46c58f9..0c43c353 100644 --- a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx +++ b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx @@ -19,6 +19,7 @@ import { Spinner } from "@postgres.ai/shared/components/Spinner"; import { HeaderButtons, HeaderButtonsProps } from "../HeaderButtons/HeaderButtons"; import { BotMessage } from "../../../types/api/entities/bot"; import { theme } from "@postgres.ai/shared/styles/theme"; +import { useAiBot } from "../hooks"; const useStyles = makeStyles((theme) => ({ @@ -30,7 +31,7 @@ const useStyles = makeStyles((theme) => ({ [theme.breakpoints.down('sm')]: { height: '100vh!important', marginTop: '0!important', - width: 300, + width: 320, zIndex: 9999 }, '& > ul': { @@ -54,6 +55,9 @@ const useStyles = makeStyles((theme) => ({ }, listSubheaderRoot: { background: 'white', + [theme.breakpoints.down('sm')]: { + padding: 0 + } }, listItemLink: { fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', @@ -91,8 +95,6 @@ type ChatsListProps = { onCreateNewChat: () => void; onClose: () => void; isDemoOrg: boolean; - loading: boolean; - chatsList: BotMessage[] | null; onLinkClick?: (targetThreadId: string) => void; } & HeaderButtonsProps @@ -101,13 +103,14 @@ export const ChatsList = (props: ChatsListProps) => { isOpen, onCreateNewChat, onClose, - chatsList, - loading, withChatVisibilityButton, onSettingsClick, onLinkClick, onConsoleClick } = props; + + const { chatsList, chatsListLoading: loading } = useAiBot(); + const classes = useStyles(props); const params = useParams<{ org?: string, threadId?: string }>(); const matches = useMediaQuery(theme.breakpoints.down('sm')); diff --git a/ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx b/ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx index a0ab2db9..7e74a8fa 100644 --- a/ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx +++ b/ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx @@ -15,6 +15,12 @@ const useStyles = makeStyles( left: 'unset!important', height: '80vh', width: '80vw', + [theme.breakpoints.down('sm')]: { + right: 'unset!important', + top: '0!important', + height: '100vh', + width: '100vw', + } }, paper: { width: '80vw', @@ -23,6 +29,12 @@ const useStyles = makeStyles( transition: '.2s ease', '&:hover': { opacity: 1 + }, + [theme.breakpoints.down('sm')]: { + opacity: 1, + width: '100vw', + height: '100vh', + margin: '0' } }, dialogTitle: { diff --git a/ui/packages/platform/src/pages/Bot/HeaderButtons/HeaderButtons.tsx b/ui/packages/platform/src/pages/Bot/HeaderButtons/HeaderButtons.tsx index 5577219a..91de9438 100644 --- a/ui/packages/platform/src/pages/Bot/HeaderButtons/HeaderButtons.tsx +++ b/ui/packages/platform/src/pages/Bot/HeaderButtons/HeaderButtons.tsx @@ -44,7 +44,7 @@ const useStyles = makeStyles((theme) => ({ minWidth: '2rem', height: '2rem', padding: 0, - marginRight: '0.5rem', + marginRight: '0.25rem', '& .MuiButton-startIcon': { margin: 0 } diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx index 8d79ae72..b062cda9 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx @@ -18,12 +18,14 @@ type BaseMessageProps = { content?: string; name?: string; isLoading?: boolean; - formattedTime?: string + formattedTime?: string; + aiModel?: string } type AiMessageProps = BaseMessageProps & { isAi: true; content: string; + aiModel: string } type HumanMessageProps = BaseMessageProps & { @@ -121,7 +123,7 @@ const useStyles = makeStyles( flexWrap: 'wrap', alignItems: 'baseline', '@media (max-width: 450px)': { - height: '2.25rem', + height: 'auto', } }, additionalInfo: { @@ -239,6 +241,7 @@ export const Message = React.memo((props: MessageProps) => { name, created_at, isLoading, + aiModel } = props; const [isDebugVisible, setDebugVisible] = useState(false); @@ -315,12 +318,23 @@ export const Message = React.memo((props: MessageProps) => { debug info } + { + aiModel && isAi && <> + | + + {aiModel} + + + }
{isLoading ?
-
+
Thinking
diff --git a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx index d85a0bc6..f03d6b92 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx @@ -244,6 +244,7 @@ export const Messages = React.memo(() => { slack_profile, created_at, content, + ai_model } = message; let name = 'You'; @@ -270,11 +271,12 @@ export const Messages = React.memo(() => { created_at={created_at} content={content} formattedTime={formattedTime} + aiModel={ai_model} /> ) })} {isWaitingForAnswer && - + }
diff --git a/ui/packages/platform/src/pages/Bot/PublicChatDialog/PublicChatDialog.tsx b/ui/packages/platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx similarity index 65% rename from ui/packages/platform/src/pages/Bot/PublicChatDialog/PublicChatDialog.tsx rename to ui/packages/platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx index cd1e902a..7ddad90a 100644 --- a/ui/packages/platform/src/pages/Bot/PublicChatDialog/PublicChatDialog.tsx +++ b/ui/packages/platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx @@ -5,7 +5,7 @@ *-------------------------------------------------------------------------- */ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { IconButton, TextField, @@ -20,12 +20,15 @@ import { import MuiDialogTitle from '@material-ui/core/DialogTitle' import MuiDialogContent from '@material-ui/core/DialogContent' import MuiDialogActions from '@material-ui/core/DialogActions' - +import FormLabel from '@mui/material/FormLabel' import { styles } from '@postgres.ai/shared/styles/styles' import { icons } from '@postgres.ai/shared/styles/icons' import { Spinner } from '@postgres.ai/shared/components/Spinner' import { colors } from "@postgres.ai/shared/styles/colors"; import { useAiBot } from "../hooks"; +import { AiModel } from "../../../types/api/entities/bot"; + +export type Visibility = 'public' | 'private'; type DialogTitleProps = { id: string @@ -36,9 +39,7 @@ type DialogTitleProps = { type PublicChatDialogProps = { isOpen: boolean onClose: () => void - onSaveChanges: (value: boolean) => void - isLoading: boolean - threadId: string + threadId: string | null } const useDialogTitleStyles = makeStyles( @@ -158,11 +159,25 @@ const useDialogStyles = makeStyles( { index: 1 }, ) -export const PublicChatDialog = (props: PublicChatDialogProps) => { - const { onSaveChanges, onClose, isOpen, isLoading, threadId } = props; +export const SettingsDialog = (props: PublicChatDialogProps) => { + const { + onClose, + isOpen, + threadId, + } = props; + - const { chatVisibility } = useAiBot(); + const { + chatVisibility, + changeChatVisibility, + isChangeVisibilityLoading, + getChatsList, + aiModels, + aiModel: activeModel, + setAiModel: setActiveModel, + } = useAiBot(); + const [model, setModel] = useState(activeModel) const [visibility, setVisibility] = useState(chatVisibility); const classes = useDialogStyles(); @@ -176,11 +191,28 @@ export const PublicChatDialog = (props: PublicChatDialogProps) => { } const handleSaveChanges = () => { - if (chatVisibility !== visibility) { - onSaveChanges(visibility === 'public'); + if (model && model !== activeModel) { + setActiveModel(model) } + if (visibility !== chatVisibility && threadId) { + changeChatVisibility(threadId, visibility === 'public') + getChatsList(); + } + + onClose() } + useEffect(() => { + if (isOpen) { + if (visibility !== chatVisibility) { + setVisibility(chatVisibility) + } + if (model?.name !== activeModel?.name) { + setModel(activeModel) + } + } + }, [isOpen]); + const urlField = (
{ aria-labelledby="customized-dialog-title" open={isOpen} className={classes.dialog} + fullWidth > - Public Chat + Chat Settings - { - setVisibility(event.target.value) - }} - className={classes.radioLabel} - > - } - label="Only members of the organization can view" - /> + {threadId && <> + Visibility + { + setVisibility(event.target.value as Visibility) + }} + className={classes.radioLabel} + > + } + label="Only members of the organization can view" + /> - } - label="Anyone with a special link and members of the organization can view" - /> - - {visibility && ( -
{urlField}
- )} + } + label="Anyone with a special link and members of the organization can view" + /> +
+ {visibility && ( +
{urlField}
+ )} + } + {aiModels && <> + Model + { + const selectedModel = aiModels?.find((model) => `${model.vendor}/${model.name}` === event.target.value) + setModel(selectedModel!) + }} + className={classes.radioLabel} + > + {aiModels.map((model) => + } + label={model.name} + /> + ) + } + + }
+ + ) + } + return ( <> -
- This thread is {chatVisibility} - + {chatVisibility} thread + }
: { - const { messages, loading: isLoading, wsLoading: isWaitingForAnswer } = useAiBot(); + const { messages, loading: isLoading, wsLoading: isWaitingForAnswer, stateMessage } = useAiBot(); const rootRef = useRef(null); const wrapperRef = useRef(null); @@ -276,7 +276,7 @@ export const Messages = React.memo(() => { ) })} {isWaitingForAnswer && - + }
diff --git a/ui/packages/platform/src/pages/Bot/hooks.tsx b/ui/packages/platform/src/pages/Bot/hooks.tsx index 6269eb87..ea189f85 100644 --- a/ui/packages/platform/src/pages/Bot/hooks.tsx +++ b/ui/packages/platform/src/pages/Bot/hooks.tsx @@ -8,7 +8,7 @@ import React, { createContext, useCallback, useContext, useEffect, useState } from "react"; import useWebSocket, {ReadyState} from "react-use-websocket"; import { useLocation } from "react-router-dom"; -import { BotMessage, DebugMessage, AiModel } from "../../types/api/entities/bot"; +import { BotMessage, DebugMessage, AiModel, StateMessage } from "../../types/api/entities/bot"; import {getChatsWithWholeThreads} from "../../api/bot/getChatsWithWholeThreads"; import {getChats} from "api/bot/getChats"; import {useAlertSnackbar} from "@postgres.ai/shared/components/AlertSnackbar/useAlertSnackbar"; @@ -45,18 +45,19 @@ type UseAiBotReturnType = { wsReadyState: ReadyState; changeChatVisibility: (threadId: string, isPublic: boolean) => void; isChangeVisibilityLoading: boolean; - unsubscribe: (threadId: string) => void - chatVisibility: 'public' | 'private' - debugMessages: DebugMessage[] | null - getDebugMessagesForWholeThread: () => void + unsubscribe: (threadId: string) => void; + chatVisibility: 'public' | 'private'; + debugMessages: DebugMessage[] | null; + getDebugMessagesForWholeThread: () => void; chatsList: UseBotChatsListHook['chatsList']; chatsListLoading: UseBotChatsListHook['loading']; - getChatsList: UseBotChatsListHook['getChatsList'] - aiModel: UseAiModelsList['aiModel'], - setAiModel: UseAiModelsList['setAiModel'] - aiModels: UseAiModelsList['aiModels'] - aiModelsLoading: UseAiModelsList['loading'] + getChatsList: UseBotChatsListHook['getChatsList']; + aiModel: UseAiModelsList['aiModel']; + setAiModel: UseAiModelsList['setAiModel']; + aiModels: UseAiModelsList['aiModels']; + aiModelsLoading: UseAiModelsList['loading']; debugMessagesLoading: boolean; + stateMessage: StateMessage | null; } type UseAiBotArgs = { @@ -88,6 +89,7 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => const [error, setError] = useState(null); const [wsLoading, setWsLoading] = useState(false); const [chatVisibility, setChatVisibility] = useState('public'); + const [stateMessage, setStateMessage] = useState(null) const [isChangeVisibilityLoading, setIsChangeVisibilityLoading] = useState(false); @@ -100,16 +102,25 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => const onWebSocketMessage = (event: WebSocketEventMap['message']) => { if (event.data) { - const messageData: BotMessage | DebugMessage = JSON.parse(event.data); + const messageData: BotMessage | DebugMessage | StateMessage = JSON.parse(event.data); if (messageData) { const isThreadMatching = threadId && threadId === messageData.thread_id; const isParentMatching = !threadId && 'parent_id' in messageData && messageData.parent_id && messages; const isDebugMessage = messageData.type === 'debug'; - if (isThreadMatching || isParentMatching || isDebugMessage) { + const isStateMessage = messageData.type === 'state'; + if (isThreadMatching || isParentMatching || isDebugMessage || isStateMessage) { if (isDebugMessage) { let currentDebugMessages = [...(debugMessages || [])]; currentDebugMessages.push(messageData) setDebugMessages(currentDebugMessages) + } else if (isStateMessage) { + if (isThreadMatching || !threadId) { + if (messageData.state) { + setStateMessage(messageData) + } else { + setStateMessage(null) + } + } } else { // Check if the last message needs its data updated let currentMessages = [...(messages || [])]; @@ -368,7 +379,8 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => aiModelsLoading, chatVisibility, debugMessages, - debugMessagesLoading + debugMessagesLoading, + stateMessage, } } diff --git a/ui/packages/platform/src/types/api/entities/bot.ts b/ui/packages/platform/src/types/api/entities/bot.ts index 93d1b6e7..334b24df 100644 --- a/ui/packages/platform/src/types/api/entities/bot.ts +++ b/ui/packages/platform/src/types/api/entities/bot.ts @@ -34,4 +34,10 @@ export type AiModel = { vendor: string; isThirdParty: boolean; freeUseAvailable: boolean; -}; \ No newline at end of file +}; + +export type StateMessage = { + type: 'state' + state: string | null + thread_id: string +} \ No newline at end of file From 9cec7f6235af87c200fd434e8d70ccdd037f54fb Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Tue, 9 Jul 2024 16:10:42 +0000 Subject: [PATCH 040/111] Bot UI: Hide Intercom widget on Bot page --- .../platform/src/hooks/useHideIntercom.ts | 38 +++++++++++++++++++ .../platform/src/pages/Bot/BotWrapper.tsx | 2 + .../src/pages/Bot/Command/Command.tsx | 6 --- 3 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 ui/packages/platform/src/hooks/useHideIntercom.ts diff --git a/ui/packages/platform/src/hooks/useHideIntercom.ts b/ui/packages/platform/src/hooks/useHideIntercom.ts new file mode 100644 index 00000000..8667d310 --- /dev/null +++ b/ui/packages/platform/src/hooks/useHideIntercom.ts @@ -0,0 +1,38 @@ +import { useEffect } from 'react'; + +const INTERCOM_BUTTON_SELECTOR = `div[aria-label="Open Intercom Messenger"]`; +const INTERCOM_IFRAME_SELECTOR = `iframe[name="intercom-launcher-frame"]`; + +export const useHideIntercom = () => { + useEffect(() => { + const intercomButton = document.querySelector(INTERCOM_BUTTON_SELECTOR); + const intercomIframe = document.querySelector(INTERCOM_IFRAME_SELECTOR); + + const originalButtonDisplay = intercomButton ? (intercomButton as HTMLElement).style.display : ''; + const originalIframeDisplay = intercomIframe ? (intercomIframe as HTMLElement).style.display : ''; + + const hideIntercom = () => { + if (intercomButton) { + (intercomButton as HTMLElement).style.display = 'none'; + } + if (intercomIframe) { + (intercomIframe as HTMLElement).style.display = 'none'; + } + }; + + const showIntercom = () => { + if (intercomButton) { + (intercomButton as HTMLElement).style.display = originalButtonDisplay || 'block'; + } + if (intercomIframe) { + (intercomIframe as HTMLElement).style.display = originalIframeDisplay || 'inline'; + } + }; + + hideIntercom(); + + return () => { + showIntercom(); + }; + }, []); +}; \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/BotWrapper.tsx b/ui/packages/platform/src/pages/Bot/BotWrapper.tsx index d8676010..6f254e4c 100644 --- a/ui/packages/platform/src/pages/Bot/BotWrapper.tsx +++ b/ui/packages/platform/src/pages/Bot/BotWrapper.tsx @@ -2,6 +2,7 @@ import { BotPage } from "./index"; import {RouteComponentProps} from "react-router"; import {AlertSnackbarProvider} from "@postgres.ai/shared/components/AlertSnackbar/useAlertSnackbar"; import { AiBotProvider } from "./hooks"; +import { useHideIntercom } from "../../hooks/useHideIntercom"; export interface BotWrapperProps { envData: { @@ -24,6 +25,7 @@ export interface BotWrapperProps { export const BotWrapper = (props: BotWrapperProps) => { + useHideIntercom(); return ( diff --git a/ui/packages/platform/src/pages/Bot/Command/Command.tsx b/ui/packages/platform/src/pages/Bot/Command/Command.tsx index 18f8b814..7c100aca 100644 --- a/ui/packages/platform/src/pages/Bot/Command/Command.tsx +++ b/ui/packages/platform/src/pages/Bot/Command/Command.tsx @@ -194,11 +194,6 @@ export const Command = React.memo((props: Props) => { setValue('') }, [threadId]); - // Floating intercom. - const sendButtonRef = useRef(null) - - useFloatingIntercom(sendButtonRef) - return (
{ onClick={triggerSend} className={classes.iconButton} disabled={sendDisabled || value.length === 0} - ref={sendButtonRef} > From 41915ac7950f38936b48d55eb242a0eca2e23b84 Mon Sep 17 00:00:00 2001 From: Nikolay Samokhvalov Date: Tue, 9 Jul 2024 16:11:34 +0000 Subject: [PATCH 041/111] Demo/API example: use /api prefix with UI port --- engine/api/swagger-spec/dblab_server_swagger.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/api/swagger-spec/dblab_server_swagger.yaml b/engine/api/swagger-spec/dblab_server_swagger.yaml index 2ca26850..cf93cead 100644 --- a/engine/api/swagger-spec/dblab_server_swagger.yaml +++ b/engine/api/swagger-spec/dblab_server_swagger.yaml @@ -21,7 +21,7 @@ externalDocs: url: https://gitlab.com/postgres-ai/docs/tree/master/docs/database-lab servers: - - url: "https://demo.aws.postgres.ai:446" + - url: "https://demo.aws.postgres.ai/api" description: "DBLab 3.x demo server; token: 'demo-token'" x-examples: Verification-Token: "demo-token" From 131e3e2ab4d0a2123643fbcf5af7a4eaf10cfb09 Mon Sep 17 00:00:00 2001 From: Lasha Kakabadze Date: Fri, 12 Jul 2024 15:51:45 +0000 Subject: [PATCH 042/111] feat(ui): postgres ha cluster - "extensions" (https://gitlab.com/postgres-ai/database-lab/-/issues/548) and "filesystem" (https://gitlab.com/postgres-ai/database-lab/-/issues/549) --- ui/cspell.json | 9 +- .../platform/src/api/configs/launchDeploy.ts | 123 +++--- .../AddDbLabInstanceFormWrapper.tsx | 5 +- .../DbLabFormSteps/AnsibleInstance.tsx | 20 +- .../DbLabFormSteps/DockerInstance.tsx | 24 +- .../DbLabFormSteps/SimpleInstance.tsx | 23 +- .../DbLabInstanceForm/DbLabInstanceForm.tsx | 259 +++--------- .../DbLabInstanceFormSidebar.tsx | 6 +- .../DbLabInstanceFormWrapper.tsx | 5 +- .../DbLabInstanceForm/reducer/index.tsx | 42 +- .../DbLabInstanceForm/utils/index.tsx | 90 ++--- .../DbLabFormSteps/AnsibleInstance.tsx | 31 +- .../DbLabFormSteps/DockerInstance.tsx | 31 +- .../DbLabFormSteps/SetupStep.tsx | 22 ++ .../DbLabInstanceInstallFormSidebar.tsx | 4 +- .../DbLabInstanceInstallFormWrapper.tsx | 6 +- .../DbLabInstanceInstallForm/utils/index.ts | 52 +-- .../DbLabInstances/DbLabInstancesWrapper.tsx | 7 +- .../DbLabSession/DbLabSessionWrapper.tsx | 6 +- .../src/components/JoeHistory/JoeHistory.tsx | 4 +- .../OrgMembers/OrgMembersWrapper.tsx | 7 +- .../PostgresClusterForm/PostgresCluster.tsx | 367 +++++++----------- .../PostgresClusterSteps/index.tsx | 252 ++++++++++++ .../PostgresClusterWrapper.tsx | 38 +- .../PostgresClusterForm/reducer/index.tsx | 116 ++++-- .../PostgresClusterForm/utils/index.tsx | 56 +-- .../PostgresClusterInstallForm.tsx | 13 +- .../PostgresClusterInstallFormSidebar.tsx | 4 +- .../PostgresClusterInstallWrapper.tsx | 5 +- .../PostgresClusterSteps/AnsibleInstance.tsx | 18 +- .../PostgresClusterSteps/DockerInstance.tsx | 14 +- .../reducer/index.tsx | 83 +++- .../utils/index.tsx | 89 +++-- .../PostgresClusters/PostgresClusters.tsx | 63 ++- .../PostgresClustersWrapper.tsx | 7 +- .../src/components/Reports/ReportsWrapper.tsx | 6 +- .../src/components/StripeForm/index.tsx | 14 +- .../platform/src/hooks/useCloudProvider.ts | 183 +++++++++ ui/packages/shared/icons/Renewable/index.tsx | 6 - ui/packages/shared/icons/Shield/index.tsx | 1 - 40 files changed, 1208 insertions(+), 903 deletions(-) create mode 100644 ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/SetupStep.tsx create mode 100644 ui/packages/platform/src/components/PostgresClusterForm/PostgresClusterSteps/index.tsx create mode 100644 ui/packages/platform/src/hooks/useCloudProvider.ts diff --git a/ui/cspell.json b/ui/cspell.json index 4ef29a5a..d7299b16 100644 --- a/ui/cspell.json +++ b/ui/cspell.json @@ -186,6 +186,13 @@ "pgnode", "pgbackrest", "vitabaks", - "distro" + "distro", + "pgaudit", + "pgrouting", + "timescaledb", + "citus", + "pgvector", + "partman", + "fstype" ] } diff --git a/ui/packages/platform/src/api/configs/launchDeploy.ts b/ui/packages/platform/src/api/configs/launchDeploy.ts index 6b3b04b4..b593d98d 100644 --- a/ui/packages/platform/src/api/configs/launchDeploy.ts +++ b/ui/packages/platform/src/api/configs/launchDeploy.ts @@ -1,6 +1,9 @@ +import { + DEBUG_API_SERVER, + sePackageTag, +} from 'components/DbLabInstanceForm/utils' import { simpleInstallRequest } from 'helpers/simpleInstallRequest' -import { initialState } from 'components/DbLabInstanceForm/reducer' -import { DEBUG_API_SERVER, sePackageTag } from 'components/DbLabInstanceForm/utils' +import { useCloudProviderProps } from 'hooks/useCloudProvider' const API_SERVER = process.env.REACT_APP_API_SERVER @@ -23,7 +26,7 @@ export const launchDeploy = async ({ cloudImage, launchType, }: { - state: typeof initialState + state: useCloudProviderProps['initialState'] orgKey: string userID?: number extraEnvs: { @@ -32,6 +35,26 @@ export const launchDeploy = async ({ cloudImage: string launchType: 'cluster' | 'instance' }) => { + const instanceExtraVars = [ + `provision=${state.provider}`, + `server_name=${state.name}`, + `platform_project_name=${state.name}`, + `server_type=${state.instanceType.native_name}`, + `server_image=${cloudImage}`, + `server_location=${state.location.native_code}`, + `volume_size=${state.storage}`, + `dblab_engine_version=${state.tag}`, + `zpool_datasets_number=${state.snapshots}`, + `dblab_engine_verification_token=${state.verificationToken}`, + `platform_org_key=${orgKey}`, + ] + + const instanceOptionalVars = [ + state.publicKeys && `ssh_public_keys="${state.publicKeys}"`, + API_SERVER === DEBUG_API_SERVER && + `platform_url=https://v2.postgres.ai/api/general`, + ].filter(Boolean) + const instanceBody = { playbook: 'deploy_dle.yml', provision: state.provider, @@ -42,29 +65,56 @@ export const launchDeploy = async ({ location: state.location.native_code, }, image: `postgresai/dle-se-ansible:${sePackageTag}`, - extraVars: [ - `provision=${state.provider}`, - `server_name=${state.name}`, - `platform_project_name=${state.name}`, - `server_type=${state.instanceType.native_name}`, - `server_image=${cloudImage}`, - `server_location=${state.location.native_code}`, - `volume_size=${state.storage}`, - `dblab_engine_version=${state.tag}`, - `zpool_datasets_number=${state.snapshots}`, - `dblab_engine_verification_token=${state.verificationToken}`, - `platform_org_key=${orgKey}`, - ...(state.publicKeys - ? // eslint-disable-next-line no-useless-escape - [`ssh_public_keys=\"${state.publicKeys}\"`] - : []), - ...(API_SERVER === DEBUG_API_SERVER - ? [`platform_url=https://v2.postgres.ai/api/general`] - : []), - ], + extraVars: [...instanceExtraVars, ...instanceOptionalVars], extraEnvs: formatExtraEnvs(extraEnvs), } + const user = state.provider === 'aws' ? 'ubuntu' : 'root' + + const extraVars = [ + `ansible_user=${user}`, + `provision=${state.provider}`, + `servers_count=${state.numberOfInstances}`, + `server_type=${state.instanceType.native_name}`, + `server_image=${cloudImage}`, + `server_location=${state.location.native_code}`, + `volume_size=${state.storage}`, + `postgresql_version=${state.version}`, + `database_public_access=${state.database_public_access}`, + `with_haproxy_load_balancing=${state.with_haproxy_load_balancing}`, + `pgbouncer_install=${state.pgbouncer_install}`, + `pg_data_mount_fstype=${state.fileSystem}`, + `synchronous_mode=${state.synchronous_mode}`, + `netdata_install=${state.netdata_install}`, + `patroni_cluster_name=${state.name}`, + `platform_org_key=${orgKey}`, + ] + + const optionalVars = [ + state.synchronous_mode && + `synchronous_node_count=${state.synchronous_node_count}`, + state.pg_repack && `enable_pg_repack=${state.pg_repack}`, + state.pg_cron && `enable_pg_cron=${state.pg_cron}`, + state.pgaudit && `enable_pgaudit=${state.pgaudit}`, + state.version !== 10 && + state.pgvector && + `enable_pgvector=${state.pgvector}`, + state.postgis && `enable_postgis=${state.postgis}`, + state.pgrouting && `enable_pgrouting=${state.pgrouting}`, + state.version !== 10 && + state.version !== 11 && + state.timescaledb && + `enable_timescaledb=${state.timescaledb}`, + state.version !== 10 && state.citus && `enable_citus=${state.citus}`, + state.pg_partman && `enable_pg_partman=${state.pg_partman}`, + state.pg_stat_kcache && `enable_pg_stat_kcache=${state.pg_stat_kcache}`, + state.pg_wait_sampling && + `enable_pg_wait_sampling=${state.pg_wait_sampling}`, + state.publicKeys && `ssh_public_keys="${state.publicKeys}"`, + API_SERVER === DEBUG_API_SERVER && + `platform_url=https://v2.postgres.ai/api/general`, + ].filter(Boolean) + const clusterBody = { playbook: 'deploy_pgcluster.yml', provision: state.provider, @@ -75,32 +125,7 @@ export const launchDeploy = async ({ location: state.location.native_code, }, image: 'vitabaks/postgresql_cluster:cloud', - extraVars: [ - `ansible_user=${state.provider === "aws" ? 'ubuntu' : 'root'}`, - `provision=${state.provider}`, - `servers_count=${state.numberOfInstances}`, - `server_type=${state.instanceType.native_name}`, - `server_image=${cloudImage}`, - `server_location=${state.location.native_code}`, - `volume_size=${state.storage}`, - `postgresql_version=${state.version}`, - `database_public_access=${state.database_public_access}`, - `database_public_access=${state.database_public_access}`, - `with_haproxy_load_balancing=${state.with_haproxy_load_balancing}`, - `pgbouncer_install=${state.pgbouncer_install}`, - `synchronous_mode=${state.synchronous_mode}`, - ...(state.synchronous_mode ? [`synchronous_node_count=${state.synchronous_node_count}`] : []), - `netdata_install=${state.netdata_install}`, - `patroni_cluster_name=${state.name}`, - `platform_org_key=${orgKey}`, - ...(state.publicKeys - ? // eslint-disable-next-line no-useless-escape - [`ssh_public_keys=\"${state.publicKeys}\"`] - : []), - ...(API_SERVER === DEBUG_API_SERVER - ? [`platform_url=https://v2.postgres.ai/api/general`] - : []), - ], + extraVars: [...extraVars, ...optionalVars], extraEnvs: formatExtraEnvs(extraEnvs), } diff --git a/ui/packages/platform/src/components/AddDbLabInstanceFormWrapper/AddDbLabInstanceFormWrapper.tsx b/ui/packages/platform/src/components/AddDbLabInstanceFormWrapper/AddDbLabInstanceFormWrapper.tsx index b38ac7fe..8800ba33 100644 --- a/ui/packages/platform/src/components/AddDbLabInstanceFormWrapper/AddDbLabInstanceFormWrapper.tsx +++ b/ui/packages/platform/src/components/AddDbLabInstanceFormWrapper/AddDbLabInstanceFormWrapper.tsx @@ -1,6 +1,7 @@ import { makeStyles } from '@material-ui/core' import { styles } from '@postgres.ai/shared/styles/styles' import AddDbLabInstanceForm from 'components/AddDbLabInstanceFormWrapper/AddDblabInstanceForm' +import { OrgPermissions } from 'components/types' import { RouteComponentProps } from 'react-router' export interface DbLabInstanceFormProps { @@ -8,9 +9,7 @@ export interface DbLabInstanceFormProps { orgId: number project: string | undefined history: RouteComponentProps['history'] - orgPermissions: { - dblabInstanceCreate?: boolean - } + orgPermissions: OrgPermissions } export const AddDbLabInstanceFormWrapper = (props: DbLabInstanceFormProps) => { diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx index deaf2191..4fbc442b 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx @@ -1,30 +1,30 @@ +import { Button, makeStyles } from '@material-ui/core' import { Box } from '@mui/material' import { useEffect, useState } from 'react' -import { makeStyles, Button } from '@material-ui/core' -import { Spinner } from '@postgres.ai/shared/components/Spinner' import { ErrorStub } from '@postgres.ai/shared/components/ErrorStub' +import { Spinner } from '@postgres.ai/shared/components/Spinner' import { SyntaxHighlight } from '@postgres.ai/shared/components/SyntaxHighlight' -import { getOrgKeys } from 'api/cloud/getOrgKeys' import { getCloudImages } from 'api/cloud/getCloudImages' +import { getOrgKeys } from 'api/cloud/getOrgKeys' +import { InstanceFormCreation } from 'components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation' import { getGcpAccountContents, getNetworkSubnet, - getPlaybookCommandWithoutDocker, + getPlaybookCommand, } from 'components/DbLabInstanceForm/utils' import { cloneRepositoryCommand, getAnsibleInstallationCommand, } from 'components/DbLabInstanceInstallForm/utils' -import { InstanceFormCreation } from 'components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation' -import { initialState } from '../reducer' import { cloneClusterRepositoryCommand, - getClusterPlaybookCommandWithoutDocker, + getClusterPlaybookCommand, } from 'components/PostgresClusterForm/utils' +import { useCloudProviderProps } from 'hooks/useCloudProvider' export const formStyles = makeStyles({ marginTop: { @@ -128,7 +128,7 @@ export const AnsibleInstance = ({ setFormStep, }: { cluster?: boolean - state: typeof initialState + state: useCloudProviderProps["initialState"] orgId: number goBack: () => void goBackToForm: () => void @@ -280,12 +280,12 @@ export const AnsibleInstance = ({ {getNetworkSubnet(state.provider, classes)} diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/DockerInstance.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/DockerInstance.tsx index 18eb8585..d670be30 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/DockerInstance.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/DockerInstance.tsx @@ -1,27 +1,27 @@ +import { Button } from '@material-ui/core' import { Box } from '@mui/material' import { useEffect, useState } from 'react' -import { Button } from '@material-ui/core' -import { Spinner } from '@postgres.ai/shared/components/Spinner' import { ErrorStub } from '@postgres.ai/shared/components/ErrorStub' +import { Spinner } from '@postgres.ai/shared/components/Spinner' import { SyntaxHighlight } from '@postgres.ai/shared/components/SyntaxHighlight' -import { getOrgKeys } from 'api/cloud/getOrgKeys' import { getCloudImages } from 'api/cloud/getCloudImages' +import { getOrgKeys } from 'api/cloud/getOrgKeys' -import { - getNetworkSubnet, - getGcpAccountContents, - getPlaybookCommand, -} from 'components/DbLabInstanceForm/utils' import { InstanceDocumentation, formStyles, } from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' import { InstanceFormCreation } from 'components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation' +import { + getGcpAccountContents, + getNetworkSubnet, + getPlaybookCommand, +} from 'components/DbLabInstanceForm/utils' -import { initialState } from '../reducer' import { getClusterPlaybookCommand } from 'components/PostgresClusterForm/utils' +import { useCloudProviderProps } from 'hooks/useCloudProvider' export const DockerInstance = ({ cluster, @@ -32,7 +32,7 @@ export const DockerInstance = ({ formStep, setFormStep, }: { - state: typeof initialState + state: useCloudProviderProps['initialState'] cluster?: boolean orgId: number goBack: () => void @@ -150,8 +150,8 @@ export const DockerInstance = ({ {getNetworkSubnet(state.provider, classes)} diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/SimpleInstance.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/SimpleInstance.tsx index b288387a..6b0e52d0 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/SimpleInstance.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/SimpleInstance.tsx @@ -1,23 +1,23 @@ -import { Box } from '@mui/material' import { Button, TextField } from '@material-ui/core' +import { Box } from '@mui/material' import { useCallback, useEffect, useState } from 'react' -import { Spinner } from '@postgres.ai/shared/components/Spinner' import { ErrorStub } from '@postgres.ai/shared/components/ErrorStub' +import { Spinner } from '@postgres.ai/shared/components/Spinner' import { SyntaxHighlight } from '@postgres.ai/shared/components/SyntaxHighlight' -import { formStyles } from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' import { ResponseMessage } from '@postgres.ai/shared/pages/Configuration/ResponseMessage' +import { formStyles } from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' import { InstanceFormCreation } from 'components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation' -import { initialState } from '../reducer' -import { cloudProviderName } from '../utils' +import { useWsScroll } from '@postgres.ai/shared/pages/Logs/hooks/useWsScroll' +import { getCloudImages } from 'api/cloud/getCloudImages' import { getOrgKeys } from 'api/cloud/getOrgKeys' -import { establishConnection } from './streamLogs' +import { getTaskState } from 'api/configs/getTaskState' import { launchDeploy } from 'api/configs/launchDeploy' -import { getCloudImages } from 'api/cloud/getCloudImages' import { regenerateCode } from 'api/configs/regenerateCode' -import { useWsScroll } from '@postgres.ai/shared/pages/Logs/hooks/useWsScroll' -import { getTaskState } from 'api/configs/getTaskState' +import { useCloudProviderProps } from 'hooks/useCloudProvider' +import { cloudProviderName } from '../utils' +import { establishConnection } from './streamLogs' const SimpleInstanceDocumentation = ({ state, @@ -30,7 +30,7 @@ const SimpleInstanceDocumentation = ({ isLoading: boolean documentation: string secondStep: JSX.Element - state: typeof initialState + state: useCloudProviderProps['initialState'] handleDeploy: (e: React.FormEvent) => void deployingState: { status: string @@ -121,7 +121,7 @@ export const SimpleInstance = ({ setFormStep, }: { cluster?: boolean - state: typeof initialState + state: useCloudProviderProps['initialState'] orgId: number userID?: number goBackToForm: () => void @@ -349,6 +349,7 @@ export const SimpleInstance = ({ }, [ state, + cluster, extraEnvs, orgKey, cloudImages, diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceForm.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceForm.tsx index a7b5a73d..af7e76ec 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceForm.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceForm.tsx @@ -5,43 +5,50 @@ *-------------------------------------------------------------------------- */ -import cn from 'classnames' -import { useEffect, useReducer } from 'react' -import { Box } from '@mui/material' import { + Button, + InputAdornment, + MenuItem, Tab, Tabs, TextField, - Button, - MenuItem, - InputAdornment, } from '@material-ui/core' +import { Box } from '@mui/material' +import cn from 'classnames' -import ConsolePageTitle from './../ConsolePageTitle' -import { TabPanel } from 'pages/JoeSessionCommand/TabPanel' -import { WarningWrapper } from 'components/Warning/WarningWrapper' import { ClassesType } from '@postgres.ai/platform/src/components/types' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { DbLabInstanceFormProps } from 'components/DbLabInstanceForm/DbLabInstanceFormWrapper' -import { StorageSlider } from 'components/DbLabInstanceForm/DbLabInstanceFormSlider' -import { CloudProvider, getCloudProviders } from 'api/cloud/getCloudProviders' -import { CloudVolumes, getCloudVolumes } from 'api/cloud/getCloudVolumes' -import { initialState, reducer } from 'components/DbLabInstanceForm/reducer' -import { DbLabInstanceFormSidebar } from 'components/DbLabInstanceForm/DbLabInstanceFormSidebar' +import { Select } from '@postgres.ai/shared/components/Select' import { Spinner } from '@postgres.ai/shared/components/Spinner' import { StubSpinner } from '@postgres.ai/shared/components/StubSpinnerFlex' -import { Select } from '@postgres.ai/shared/components/Select' -import { generateToken, validateDLEName } from 'utils/utils' -import urls from 'utils/urls' +import { CloudInstance } from 'api/cloud/getCloudInstances' +import { CloudProvider } from 'api/cloud/getCloudProviders' +import { CloudRegion } from 'api/cloud/getCloudRegions' +import { CloudVolumes } from 'api/cloud/getCloudVolumes' +import { TabPanel } from 'pages/JoeSessionCommand/TabPanel' +import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' import { AnsibleInstance } from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' -import { CloudRegion, getCloudRegions } from 'api/cloud/getCloudRegions' -import { CloudInstance, getCloudInstances } from 'api/cloud/getCloudInstances' +import { DbLabInstanceFormSidebar } from 'components/DbLabInstanceForm/DbLabInstanceFormSidebar' +import { StorageSlider } from 'components/DbLabInstanceForm/DbLabInstanceFormSlider' +import { DbLabInstanceFormProps } from 'components/DbLabInstanceForm/DbLabInstanceFormWrapper' +import { initialState, reducer } from 'components/DbLabInstanceForm/reducer' +import { WarningWrapper } from 'components/Warning/WarningWrapper' import { DockerInstance } from './DbLabFormSteps/DockerInstance' -import { availableTags } from 'components/DbLabInstanceForm/utils' import { SimpleInstance } from './DbLabFormSteps/SimpleInstance' +import { + availableTags, + filteredRegions, + uniqueRegionsByProvider, +} from 'components/DbLabInstanceForm/utils' + +import urls from 'utils/urls' +import { validateDLEName } from 'utils/utils' + +import { useCloudProvider } from 'hooks/useCloudProvider' +import ConsolePageTitle from './../ConsolePageTitle' + interface DbLabInstanceFormWithStylesProps extends DbLabInstanceFormProps { classes: ClassesType auth?: { @@ -51,126 +58,18 @@ interface DbLabInstanceFormWithStylesProps extends DbLabInstanceFormProps { const DbLabInstanceForm = (props: DbLabInstanceFormWithStylesProps) => { const { classes, orgPermissions } = props - const [state, dispatch] = useReducer(reducer, initialState) - + const { + state, + dispatch, + handleChangeVolume, + handleGenerateToken, + handleReturnToForm, + handleSetFormStep, + } = useCloudProvider({ + initialState, + reducer, + }) const permitted = !orgPermissions || orgPermissions.dblabInstanceCreate - const urlParams = new URLSearchParams(window.location.search) - const urlTaskID = urlParams.get('taskID') - const urlProvider = urlParams.get('provider') - - useEffect(() => { - if (urlTaskID && urlProvider) { - dispatch({ - type: 'set_form_step', - formStep: 'simple', - taskID: urlTaskID, - provider: urlProvider, - }) - } else { - dispatch({ - type: 'set_form_step', - formStep: initialState.formStep, - provider: initialState.provider, - }) - } - }, [urlTaskID, urlProvider]) - - useEffect(() => { - const fetchCloudDetails = async () => { - dispatch({ type: 'set_is_loading', isLoading: true }) - try { - const cloudRegions = await getCloudRegions(initialState.provider) - const cloudVolumes = await getCloudVolumes(initialState.provider) - const serviceProviders = await getCloudProviders() - const ssdCloudVolumes = cloudVolumes.response.filter( - (volume: CloudVolumes) => volume.api_name === initialState?.api_name, - )[0] - - dispatch({ - type: 'set_initial_state', - cloudRegions: cloudRegions.response, - volumes: cloudVolumes.response, - volumeType: `${ssdCloudVolumes.api_name} (${ssdCloudVolumes.cloud_provider}: ${ssdCloudVolumes.native_name})`, - volumeCurrency: ssdCloudVolumes.native_reference_price_currency, - volumePricePerHour: - ssdCloudVolumes.native_reference_price_per_1000gib_per_hour, - volumePrice: - (initialState.storage * - ssdCloudVolumes.native_reference_price_per_1000gib_per_hour) / - 1000, - serviceProviders: serviceProviders.response, - isLoading: false, - }) - } catch (error) { - console.log(error) - } - } - fetchCloudDetails() - }, []) - - useEffect(() => { - const fetchUpdatedDetails = async () => { - try { - const cloudRegions = await getCloudRegions(state.provider) - const cloudVolumes = await getCloudVolumes(state.provider) - const ssdCloudVolumes = cloudVolumes.response.filter( - (volume: CloudVolumes) => volume.api_name === initialState?.api_name, - )[0] - dispatch({ - type: 'update_initial_state', - volumes: cloudVolumes.response, - volumeType: `${ssdCloudVolumes.api_name} (${ssdCloudVolumes.cloud_provider}: ${ssdCloudVolumes.native_name})`, - volumeCurrency: ssdCloudVolumes.native_reference_price_currency, - volumePricePerHour: - ssdCloudVolumes.native_reference_price_per_1000gib_per_hour, - volumePrice: - (initialState.storage * - ssdCloudVolumes.native_reference_price_per_1000gib_per_hour) / - 1000, - cloudRegions: cloudRegions.response, - }) - } catch (error) { - console.log(error) - } - } - fetchUpdatedDetails() - }, [state.api_name, state.provider]) - - useEffect(() => { - if (state.location.native_code && state.provider) { - const fetchUpdatedDetails = async () => { - dispatch({ type: 'set_is_reloading', isReloading: true }) - try { - const cloudInstances = await getCloudInstances({ - provider: state.provider, - region: state.location.native_code, - }) - - dispatch({ - type: 'update_instance_type', - cloudInstances: cloudInstances.response, - instanceType: cloudInstances.response[0], - isReloading: false, - }) - } catch (error) { - console.log(error) - } - } - fetchUpdatedDetails() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.location.native_code, state.provider]) - - const uniqueRegionsByProvider = state.cloudRegions - .map((region: CloudRegion) => region.world_part) - .filter( - (value: string, index: number, self: string) => - self.indexOf(value) === index, - ) - - const filteredRegions = state.cloudRegions.filter( - (region: CloudRegion) => region.world_part === state.region, - ) const pageTitle = const breadcrumbs = ( @@ -183,45 +82,10 @@ const DbLabInstanceForm = (props: DbLabInstanceFormWithStylesProps) => { /> ) - const handleGenerateToken = () => { - dispatch({ - type: 'change_verification_token', - verificationToken: generateToken(), - }) - } - - const handleChangeVolume = ( - event: React.ChangeEvent, - ) => { - const volumeApiName = event.target.value.split(' ')[0] - const selectedVolume = state.volumes.filter( - (volume: CloudVolumes) => volume.api_name === volumeApiName, - )[0] - - dispatch({ - type: 'change_volume_type', - volumeType: event.target.value, - volumePricePerHour: - selectedVolume.native_reference_price_per_1000gib_per_hour, - volumePrice: - (state.storage * - selectedVolume.native_reference_price_per_1000gib_per_hour) / - 1000, - }) - } - - const handleSetFormStep = (step: string) => { - dispatch({ type: 'set_form_step', formStep: step }) - } - const handleReturnToList = () => { props.history.push(urls.linkDbLabInstances(props)) } - const handleReturnToForm = () => { - dispatch({ type: 'set_form_step', formStep: initialState.formStep }) - } - const requirePublicKeys = !state.publicKeys && (state.provider === 'aws' || state.provider === 'gcp') @@ -275,6 +139,7 @@ const DbLabInstanceForm = (props: DbLabInstanceFormWithStylesProps) => { dispatch({ type: 'change_provider', provider: provider.api_name, + isReloading: true, }) } > @@ -306,7 +171,7 @@ const DbLabInstanceForm = (props: DbLabInstanceFormWithStylesProps) => { }) } > - {uniqueRegionsByProvider.map( + {uniqueRegionsByProvider(state.cloudRegions).map( (region: string, index: number) => ( {
- {filteredRegions.map((region: CloudRegion, index: number) => ( -
- dispatch({ - type: 'change_location', - location: region, - }) - } - > -

{region.api_name}

-

🏴 {region.label}

-
- ))} + {filteredRegions(state.cloudRegions, state.region).map( + (region: CloudRegion, index: number) => ( +
+ dispatch({ + type: 'change_location', + location: region, + }) + } + > +

{region.api_name}

+

🏴 {region.label}

+
+ ), + )}
{state.instanceType ? ( <> diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceFormSidebar.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceFormSidebar.tsx index 42e83f0c..cf066e12 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceFormSidebar.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceFormSidebar.tsx @@ -7,8 +7,8 @@ import { Button, makeStyles } from '@material-ui/core' -import { initialState } from 'components/DbLabInstanceForm/reducer' -import { pricingPageForProvider, cloudProviderName } from './utils' +import { useCloudProviderProps } from 'hooks/useCloudProvider' +import { cloudProviderName, pricingPageForProvider } from './utils' const MONTHLY_HOURS = 730 @@ -92,7 +92,7 @@ export const DbLabInstanceFormSidebar = ({ disabled, }: { cluster?: boolean - state: typeof initialState + state: useCloudProviderProps['initialState'] handleCreate: () => void disabled: boolean }) => { diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceFormWrapper.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceFormWrapper.tsx index b6c3aaed..c405ece2 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceFormWrapper.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceFormWrapper.tsx @@ -11,6 +11,7 @@ import { makeStyles } from '@material-ui/core' import DbLabInstanceForm from 'components/DbLabInstanceForm/DbLabInstanceForm' import { styles } from '@postgres.ai/shared/styles/styles' +import { OrgPermissions } from 'components/types' export interface DbLabInstanceFormProps { userID?: number @@ -18,9 +19,7 @@ export interface DbLabInstanceFormProps { orgId: number project: string | undefined history: RouteComponentProps['history'] - orgPermissions: { - dblabInstanceCreate?: boolean - } + orgPermissions: OrgPermissions } export const useInstanceFormStyles = makeStyles( diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/reducer/index.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/reducer/index.tsx index a1493f21..9a103227 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/reducer/index.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/reducer/index.tsx @@ -7,11 +7,12 @@ import { ReducerAction } from 'react' -import { CloudRegion } from 'api/cloud/getCloudRegions' import { CloudInstance } from 'api/cloud/getCloudInstances' +import { CloudRegion } from 'api/cloud/getCloudRegions' import { CloudVolumes } from 'api/cloud/getCloudVolumes' import { availableTags } from 'components/DbLabInstanceForm/utils' +import { clusterExtensionsState } from 'components/PostgresClusterInstallForm/reducer' export const initialState = { isLoading: false, @@ -21,9 +22,9 @@ export const initialState = { storage: 30, region: 'North America', tag: availableTags[0], - serviceProviders: [], - cloudRegions: [], - cloudInstances: [], + serviceProviders: [] as string[], + cloudRegions: [] as CloudRegion[], + cloudInstances: [] as CloudInstance[], volumes: [] as CloudVolumes[], api_name: 'ssd', databaseSize: 10, @@ -38,14 +39,16 @@ export const initialState = { publicKeys: '', verificationToken: '', numberOfInstances: 3, - version: '', + version: 16, database_public_access: false, with_haproxy_load_balancing: false, pgbouncer_install: true, synchronous_mode: false, synchronous_node_count: 1, netdata_install: true, - taskID: '' + taskID: '', + fileSystem: 'zfs', + ...clusterExtensionsState, } export const reducer = ( @@ -100,6 +103,7 @@ export const reducer = ( ...state, provider: action.provider, region: initialState.region, + isReloading: action.isReloading, databaseSize: initialState.databaseSize, snapshots: initialState.snapshots, storage: initialState.storage, @@ -118,19 +122,6 @@ export const reducer = ( location: action.location, } } - case 'change_plan': { - return { - ...state, - plan: action.plan, - size: action.size, - } - } - case 'change_size': { - return { - ...state, - size: action.size, - } - } case 'change_name': { return { ...state, @@ -180,18 +171,7 @@ export const reducer = ( storage: action.storage, } } - case 'set_is_loading': { - return { - ...state, - isLoading: action.isLoading, - } - } - case 'set_is_reloading': { - return { - ...state, - isReloading: action.isReloading, - } - } + case 'set_form_step': { return { ...state, diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx index 5bf6bdd6..7dcafee6 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx @@ -1,6 +1,9 @@ import { CloudImage } from 'api/cloud/getCloudImages' +import { CloudRegion } from 'api/cloud/getCloudRegions' +import { CloudVolumes } from 'api/cloud/getCloudVolumes' +import { addIndentForDocker } from 'components/PostgresClusterInstallForm/utils' import { ClassesType } from 'components/types' -import { initialState } from '../reducer' +import { useCloudProviderProps } from 'hooks/useCloudProvider' const API_SERVER = process.env.REACT_APP_API_SERVER export const DEBUG_API_SERVER = 'https://v2.postgres.ai/api/general' @@ -10,7 +13,6 @@ export const availableTags = ['3.5.0', '3.4.0', '4.0.0-alpha.6'] export const sePackageTag = 'v1.0' export const dockerRunCommand = (provider: string) => { - /* eslint-disable no-template-curly-in-string */ switch (provider) { case 'aws': return `docker run --rm -it \\\r @@ -35,41 +37,12 @@ export const dockerRunCommand = (provider: string) => { } export const getPlaybookCommand = ( - state: typeof initialState, + state: useCloudProviderProps['initialState'], cloudImages: CloudImage, orgKey: string, -) => - `${dockerRunCommand(state.provider)} \\\r - postgresai/dle-se-ansible:${sePackageTag} \\\r - ansible-playbook deploy_dle.yml --extra-vars \\\r - "provision='${state.provider}' \\\r - server_name='${state.name}' \\\r - server_type='${state.instanceType.native_name}' \\\r - server_image='${cloudImages?.native_os_image}' \\\r - server_location='${state.location.native_code}' \\\r - volume_size='${state.storage}' \\\r - dblab_engine_verification_token='${state.verificationToken}' \\\r - dblab_engine_version='${state.tag}' \\\r - ${ - state.snapshots > 1 - ? `zpool_datasets_number='${state.snapshots}' \\\r` - : `` - } - ${orgKey ? `platform_org_key='${orgKey}' \\\r` : ``} - ${ - API_SERVER === DEBUG_API_SERVER - ? `platform_url='${DEBUG_API_SERVER}' \\\r` - : `` - } - ${state.publicKeys ? `ssh_public_keys='${state.publicKeys}' \\\r` : ``} - platform_project_name='${state.name}'"` - -export const getPlaybookCommandWithoutDocker = ( - state: typeof initialState, - cloudImages: CloudImage, - orgKey: string, -) => - `ansible-playbook deploy_dle.yml --extra-vars \\\r + isDocker?: boolean, +) =>{ + const playbookCommand = `ansible-playbook deploy_dle.yml --extra-vars \\\r "provision='${state.provider}' \\\r server_name='${state.name}' \\\r server_type='${state.instanceType.native_name}' \\\r @@ -78,18 +51,21 @@ export const getPlaybookCommandWithoutDocker = ( volume_size='${state.storage}' \\\r dblab_engine_verification_token='${state.verificationToken}' \\\r dblab_engine_version='${state.tag}' \\\r - ${ - state.snapshots > 1 ? `zpool_datasets_number='${state.snapshots}' \\\r` : `` - } - ${orgKey ? `platform_org_key='${orgKey}' \\\r` : ``} - ${ - API_SERVER === DEBUG_API_SERVER - ? `platform_url='${DEBUG_API_SERVER}' \\\r` - : `` - } + ${ state.snapshots > 1 ? `zpool_datasets_number='${state.snapshots}' \\\r` : `` } + ${ orgKey ? `platform_org_key='${orgKey}' \\\r` : `` } + ${ API_SERVER === DEBUG_API_SERVER ? `platform_url='${DEBUG_API_SERVER}' \\\r` : `` } ${state.publicKeys ? `ssh_public_keys='${state.publicKeys}' \\\r` : ``} platform_project_name='${state.name}'"` + if (isDocker) { + return `${dockerRunCommand(state.provider)} \\\r + postgresai/dle-se-ansible:${sePackageTag} \\\r + ${addIndentForDocker(playbookCommand)}` + } else { + return playbookCommand + } +} + export const getGcpAccountContents = () => `export GCP_SERVICE_ACCOUNT_CONTENTS='{ "type": "service_account", @@ -274,3 +250,29 @@ export const getNetworkSubnet = (provider: string, classNames: ClassesType) => { ) } } + +export const uniqueRegionsByProvider = (cloudRegions: CloudRegion[]) => { + return cloudRegions + .map((region) => region.world_part) + .filter((value, index, self) => self.indexOf(value) === index) +} + +export const filteredRegions = ( + cloudRegions: CloudRegion[], + selectedRegion: string, +) => { + return cloudRegions.filter((region) => region.world_part === selectedRegion) +} + +export const formatVolumeDetails = ( + ssdCloudVolume: CloudVolumes, + storage: number, +) => ({ + volumeType: `${ssdCloudVolume.api_name} (${ssdCloudVolume.cloud_provider}: ${ssdCloudVolume.native_name})`, + volumeCurrency: ssdCloudVolume.native_reference_price_currency, + volumePricePerHour: + ssdCloudVolume.native_reference_price_per_1000gib_per_hour, + volumePrice: + (storage * ssdCloudVolume.native_reference_price_per_1000gib_per_hour) / + 1000, +}) diff --git a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/AnsibleInstance.tsx b/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/AnsibleInstance.tsx index a730ad35..6d1cb770 100644 --- a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/AnsibleInstance.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/AnsibleInstance.tsx @@ -1,22 +1,23 @@ +import { Button } from '@material-ui/core' import { Box } from '@mui/material' import { useEffect, useState } from 'react' -import { Button } from '@material-ui/core' -import { Spinner } from '@postgres.ai/shared/components/Spinner' import { ErrorStub } from '@postgres.ai/shared/components/ErrorStub' +import { Spinner } from '@postgres.ai/shared/components/Spinner' import { SyntaxHighlight } from '@postgres.ai/shared/components/SyntaxHighlight' import { getOrgKeys } from 'api/cloud/getOrgKeys' import { formStyles } from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' import { InstanceFormCreation } from 'components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation' +import { SetupStep } from 'components/DbLabInstanceInstallForm/DbLabFormSteps/SetupStep' import { cloneRepositoryCommand, getAnsibleInstallationCommand, - getAnsiblePlaybookCommand, + getPlaybookCommand, } from 'components/DbLabInstanceInstallForm/utils' -import { initialState } from '../reducer' +import { useCloudProviderProps } from 'hooks/useCloudProvider' export const AnsibleInstance = ({ state, @@ -26,7 +27,7 @@ export const AnsibleInstance = ({ formStep, setFormStep, }: { - state: typeof initialState + state: useCloudProviderProps['initialState'] orgId: number goBack: () => void goBackToForm: () => void @@ -68,23 +69,7 @@ export const AnsibleInstance = ({ /> ) : ( <> -

1. Set up your machine

-
    -
  • - Obtain a machine running Ubuntu 22.04 (although other versions - may work, we recommend using an LTS version for optimal - compatibility). -
  • -
  • - Attach an empty disk that is at least twice the size of the - database you plan to use with DBLab. -
  • -
  • - Ensure that your SSH public key is added to the machine (in - ~/.ssh/authorized_keys), - allowing for secure SSH access. -
  • -
+

2. Install Ansible on your local machine (such as a laptop)

@@ -120,7 +105,7 @@ export const AnsibleInstance = ({ you will be installing DBLab.

Please be aware:

diff --git a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/DockerInstance.tsx b/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/DockerInstance.tsx index 1c338897..d8403707 100644 --- a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/DockerInstance.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/DockerInstance.tsx @@ -1,18 +1,19 @@ +import { Button } from '@material-ui/core' import { Box } from '@mui/material' import { useEffect, useState } from 'react' -import { Button } from '@material-ui/core' -import { Spinner } from '@postgres.ai/shared/components/Spinner' import { ErrorStub } from '@postgres.ai/shared/components/ErrorStub' +import { Spinner } from '@postgres.ai/shared/components/Spinner' import { SyntaxHighlight } from '@postgres.ai/shared/components/SyntaxHighlight' import { getOrgKeys } from 'api/cloud/getOrgKeys' +import { formStyles } from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' import { InstanceFormCreation } from 'components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation' +import { SetupStep } from 'components/DbLabInstanceInstallForm/DbLabFormSteps/SetupStep' import { getPlaybookCommand } from 'components/DbLabInstanceInstallForm/utils' -import { formStyles } from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' +import { useCloudProviderProps } from 'hooks/useCloudProvider' -import { initialState } from '../reducer' export const DockerInstance = ({ state, @@ -22,7 +23,7 @@ export const DockerInstance = ({ formStep, setFormStep, }: { - state: typeof initialState + state: useCloudProviderProps['initialState'] orgId: number goBack: () => void goBackToForm: () => void @@ -64,23 +65,7 @@ export const DockerInstance = ({ /> ) : ( <> -

1. Set up your machine

-
    -
  • - Obtain a machine running Ubuntu 22.04 (although other versions - may work, we recommend using an LTS version for optimal - compatibility). -
  • -
  • - Attach an empty disk that is at least twice the size of the - database you plan to use with DBLab. -
  • -
  • - Ensure that your SSH public key is added to the machine (in - ~/.ssh/authorized_keys), - allowing for secure SSH access. -
  • -
+

2. Execute the Ansible playbook to install DBLab SE on the remote server @@ -91,7 +76,7 @@ export const DockerInstance = ({ with the specific username and IP address of the server where you will be installing DBLab.

- +

Please be aware:

The script will attempt to automatically detect an empty volume diff --git a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/SetupStep.tsx b/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/SetupStep.tsx new file mode 100644 index 00000000..abd1112a --- /dev/null +++ b/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/SetupStep.tsx @@ -0,0 +1,22 @@ +import { ClassesType } from 'components/types' + +export const SetupStep = ({ classes }: { classes: ClassesType }) => ( + <> +

1. Set up your machine

+
    +
  • + Obtain a machine running Ubuntu 22.04 (although other versions may work, + we recommend using an LTS version for optimal compatibility). +
  • +
  • + Attach an empty disk that is at least twice the size of the database you + plan to use with DBLab. +
  • +
  • + Ensure that your SSH public key is added to the machine (in + ~/.ssh/authorized_keys), allowing + for secure SSH access. +
  • +
+ +) diff --git a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormSidebar.tsx b/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormSidebar.tsx index 2917990e..44f601e9 100644 --- a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormSidebar.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormSidebar.tsx @@ -6,8 +6,8 @@ */ import { Button, makeStyles } from '@material-ui/core' +import { useCloudProviderProps } from 'hooks/useCloudProvider' -import { initialState } from 'components/DbLabInstanceForm/reducer' const useStyles = makeStyles({ boxShadow: { @@ -74,7 +74,7 @@ export const DbLabInstanceFormInstallSidebar = ({ handleCreate, disabled, }: { - state: typeof initialState + state: useCloudProviderProps['initialState'] handleCreate: () => void disabled: boolean }) => { diff --git a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormWrapper.tsx b/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormWrapper.tsx index c294d604..9e930bbf 100644 --- a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormWrapper.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormWrapper.tsx @@ -10,21 +10,19 @@ import { RouteComponentProps } from 'react-router' import DbLabInstanceInstallForm from 'components/DbLabInstanceInstallForm/DbLabInstanceInstallForm' import { useInstanceFormStyles } from 'components/DbLabInstanceForm/DbLabInstanceFormWrapper' +import { OrgPermissions } from 'components/types' export interface DbLabInstanceFormProps { edit?: boolean orgId: number project: string | undefined history: RouteComponentProps['history'] - orgPermissions: { - dblabInstanceCreate?: boolean - } + orgPermissions: OrgPermissions } export const DbLabInstanceFormInstallWrapper = ( props: DbLabInstanceFormProps, ) => { - const classes = useInstanceFormStyles() return diff --git a/ui/packages/platform/src/components/DbLabInstanceInstallForm/utils/index.ts b/ui/packages/platform/src/components/DbLabInstanceInstallForm/utils/index.ts index efad0b76..513976d3 100644 --- a/ui/packages/platform/src/components/DbLabInstanceInstallForm/utils/index.ts +++ b/ui/packages/platform/src/components/DbLabInstanceInstallForm/utils/index.ts @@ -1,43 +1,33 @@ -import { initialState } from '../reducer' -import { sePackageTag, DEBUG_API_SERVER } from 'components/DbLabInstanceForm/utils' +import { DEBUG_API_SERVER, sePackageTag } from 'components/DbLabInstanceForm/utils' +import { addIndentForDocker } from 'components/PostgresClusterInstallForm/utils' +import { useCloudProviderProps } from 'hooks/useCloudProvider' const API_SERVER = process.env.REACT_APP_API_SERVER export const getPlaybookCommand = ( - state: typeof initialState, + state: useCloudProviderProps['initialState'], orgKey: string, -) => - `docker run --rm -it \\\r + isDocker?: boolean, +) => { + const playbookCommand = `ansible-playbook deploy_dle.yml --extra-vars \\\r + "dblab_engine_host='user@server-ip-address' \\\r + platform_project_name='${state.name}' \\\r + dblab_engine_version='${state.tag}' \\\r + ${ orgKey ? `platform_org_key='${orgKey}' \\\r` : `` } + ${ API_SERVER === DEBUG_API_SERVER ? `platform_url='${DEBUG_API_SERVER}' \\\r` : `` } + ${ state.publicKeys ? `ssh_public_keys='${state.publicKeys}' \\\r` : `` } + dblab_engine_verification_token='${state.verificationToken}'"` + + if (isDocker) { + return `docker run --rm -it \\\r --volume $HOME/.ssh:/root/.ssh:ro \\\r --env ANSIBLE_SSH_ARGS="-F none" \\\r postgresai/dle-se-ansible:${sePackageTag} \\\r - ansible-playbook deploy_dle.yml --extra-vars \\\r - "dblab_host='user@server-ip-address' \\\r - platform_project_name='${state.name}' \\\r - dblab_engine_version='${state.tag}' \\\r - ${ orgKey ? `platform_org_key='${orgKey}' \\\r` : `` } - ${ API_SERVER === DEBUG_API_SERVER ? `platform_url='${DEBUG_API_SERVER}' \\\r` : `` } - ${ state.publicKeys ? `ssh_public_keys='${state.publicKeys}' \\\r` : `` } - dblab_engine_verification_token='${state.verificationToken}'" -` - -export const getAnsiblePlaybookCommand = ( - state: typeof initialState, - orgKey: string, -) => - `ansible-playbook deploy_dle.yml --extra-vars \\\r - "dblab_host='user@server-ip-address' \\\r - platform_project_name='${state.name}' \\\r - dblab_engine_version='${state.tag}' \\\r - ${orgKey ? `platform_org_key='${orgKey}' \\\r` : ``} - ${ - API_SERVER === DEBUG_API_SERVER - ? `platform_url='${DEBUG_API_SERVER}' \\\r` - : `` + ${addIndentForDocker(playbookCommand)}` + } else { + return playbookCommand } - ${state.publicKeys ? `ssh_public_keys='${state.publicKeys}' \\\r` : ``} - dblab_engine_verification_token='${state.verificationToken}'" -` +} export const getAnsibleInstallationCommand = () => `sudo apt update diff --git a/ui/packages/platform/src/components/DbLabInstances/DbLabInstancesWrapper.tsx b/ui/packages/platform/src/components/DbLabInstances/DbLabInstancesWrapper.tsx index 5433d2c8..0b75f20f 100644 --- a/ui/packages/platform/src/components/DbLabInstances/DbLabInstancesWrapper.tsx +++ b/ui/packages/platform/src/components/DbLabInstances/DbLabInstancesWrapper.tsx @@ -3,6 +3,7 @@ import { styles } from '@postgres.ai/shared/styles/styles' import DbLabInstances from 'components/DbLabInstances/DbLabInstances' import { RouteComponentProps } from 'react-router' import { colors } from '@postgres.ai/shared/styles/colors' +import { OrgPermissions } from 'components/types' export interface DbLabInstancesProps { orgId: number @@ -17,11 +18,7 @@ export interface DbLabInstancesProps { org?: string } } - orgPermissions: { - dblabInstanceCreate?: boolean - dblabInstanceDelete?: boolean - dblabInstanceList?: boolean - } + orgPermissions: OrgPermissions } export const DbLabInstancesWrapper = (props: DbLabInstancesProps) => { diff --git a/ui/packages/platform/src/components/DbLabSession/DbLabSessionWrapper.tsx b/ui/packages/platform/src/components/DbLabSession/DbLabSessionWrapper.tsx index 965d977e..781de3b8 100644 --- a/ui/packages/platform/src/components/DbLabSession/DbLabSessionWrapper.tsx +++ b/ui/packages/platform/src/components/DbLabSession/DbLabSessionWrapper.tsx @@ -2,6 +2,7 @@ import { makeStyles } from '@material-ui/core' import { colors } from '@postgres.ai/shared/styles/colors' import { styles } from '@postgres.ai/shared/styles/styles' import DbLabSession from 'components/DbLabSession/DbLabSession' +import { OrgPermissions } from 'components/types' import { RouteComponentProps } from 'react-router' interface MatchParams { @@ -9,10 +10,7 @@ interface MatchParams { } export interface DbLabSessionProps extends RouteComponentProps { - orgPermissions: { - dblabSessionView?: boolean - dblabSessionArtifactsView?: boolean - } + orgPermissions: OrgPermissions } export interface ErrorProps { diff --git a/ui/packages/platform/src/components/JoeHistory/JoeHistory.tsx b/ui/packages/platform/src/components/JoeHistory/JoeHistory.tsx index f1f6f5c3..8811aa99 100644 --- a/ui/packages/platform/src/components/JoeHistory/JoeHistory.tsx +++ b/ui/packages/platform/src/components/JoeHistory/JoeHistory.tsx @@ -278,7 +278,7 @@ class JoeHistory extends Component { contentContainer.addEventListener('scroll', () => { if ( contentContainer.scrollTop >= - contentContainer.scrollHeight - contentContainer.offsetHeight + contentContainer.scrollHeight - contentContainer.offsetHeight && !this.state.data?.commands?.isComplete ) { this.showMore() } @@ -970,7 +970,7 @@ class JoeHistory extends Component {
- {commandStore && commandStore.isProcessing && ( + {commandStore && commandStore.isProcessing && !commandStore.isComplete && ( )} {commandStore && diff --git a/ui/packages/platform/src/components/OrgMembers/OrgMembersWrapper.tsx b/ui/packages/platform/src/components/OrgMembers/OrgMembersWrapper.tsx index e97585ed..780fe321 100644 --- a/ui/packages/platform/src/components/OrgMembers/OrgMembersWrapper.tsx +++ b/ui/packages/platform/src/components/OrgMembers/OrgMembersWrapper.tsx @@ -2,17 +2,14 @@ import { makeStyles } from '@material-ui/core' import { styles } from '@postgres.ai/shared/styles/styles' import { RouteComponentProps } from 'react-router' import OrgSettings from 'components/OrgMembers/OrgMembers' +import { OrgPermissions } from 'components/types' export interface OrgSettingsProps { project: string | undefined history: RouteComponentProps['history'] org: string | number orgId: number - orgPermissions: { - settingsMemberList?: boolean - settingsMemberUpdate?: boolean - settingsMemberAdd?: boolean - } + orgPermissions: OrgPermissions env: { data: { info: { diff --git a/ui/packages/platform/src/components/PostgresClusterForm/PostgresCluster.tsx b/ui/packages/platform/src/components/PostgresClusterForm/PostgresCluster.tsx index a95ff2e3..7eb0647a 100644 --- a/ui/packages/platform/src/components/PostgresClusterForm/PostgresCluster.tsx +++ b/ui/packages/platform/src/components/PostgresClusterForm/PostgresCluster.tsx @@ -5,47 +5,51 @@ *-------------------------------------------------------------------------- */ -import cn from 'classnames' -import { useEffect, useReducer } from 'react' -import { Box } from '@mui/material' import { + Accordion, + AccordionDetails, + AccordionSummary, + Checkbox, + FormControlLabel, + InputAdornment, + MenuItem, Tab, Tabs, TextField, - MenuItem, - InputAdornment, - FormControlLabel, - Checkbox, - Accordion, - AccordionSummary, - AccordionDetails, } from '@material-ui/core' +import { Box } from '@mui/material' +import cn from 'classnames' -import ConsolePageTitle from '../ConsolePageTitle' -import { TabPanel } from 'pages/JoeSessionCommand/TabPanel' -import { WarningWrapper } from 'components/Warning/WarningWrapper' import { ClassesType } from '@postgres.ai/platform/src/components/types' +import { Select } from '@postgres.ai/shared/components/Select' +import { Spinner } from '@postgres.ai/shared/components/Spinner' +import { StubSpinner } from '@postgres.ai/shared/components/StubSpinnerFlex' +import { CloudProvider } from 'api/cloud/getCloudProviders' +import { CloudVolumes } from 'api/cloud/getCloudVolumes' import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { DbLabInstanceFormProps } from 'components/DbLabInstanceForm/DbLabInstanceFormWrapper' +import { DbLabInstanceFormSidebar } from 'components/DbLabInstanceForm/DbLabInstanceFormSidebar' import { StorageSlider } from 'components/DbLabInstanceForm/DbLabInstanceFormSlider' -import { CloudProvider, getCloudProviders } from 'api/cloud/getCloudProviders' -import { CloudVolumes, getCloudVolumes } from 'api/cloud/getCloudVolumes' +import { DbLabInstanceFormProps } from 'components/DbLabInstanceForm/DbLabInstanceFormWrapper' import { initialState, reducer } from 'components/PostgresClusterForm/reducer' -import { initialState as dbLabInitialState } from 'components/DbLabInstanceForm/reducer' -import { DbLabInstanceFormSidebar } from 'components/DbLabInstanceForm/DbLabInstanceFormSidebar' -import { Spinner } from '@postgres.ai/shared/components/Spinner' -import { StubSpinner } from '@postgres.ai/shared/components/StubSpinnerFlex' -import { Select } from '@postgres.ai/shared/components/Select' +import { WarningWrapper } from 'components/Warning/WarningWrapper' +import { TabPanel } from 'pages/JoeSessionCommand/TabPanel' +import ConsolePageTitle from '../ConsolePageTitle' -import { validateDLEName } from 'utils/utils' import urls from 'utils/urls' +import { validateDLEName } from 'utils/utils' +import { icons } from '@postgres.ai/shared/styles/icons' +import { CloudInstance } from 'api/cloud/getCloudInstances' +import { CloudRegion } from 'api/cloud/getCloudRegions' import { AnsibleInstance } from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' -import { CloudRegion, getCloudRegions } from 'api/cloud/getCloudRegions' -import { CloudInstance, getCloudInstances } from 'api/cloud/getCloudInstances' import { DockerInstance } from 'components/DbLabInstanceForm/DbLabFormSteps/DockerInstance' import { SimpleInstance } from 'components/DbLabInstanceForm/DbLabFormSteps/SimpleInstance' -import { icons } from '@postgres.ai/shared/styles/icons' +import { + filteredRegions, + uniqueRegionsByProvider, +} from 'components/DbLabInstanceForm/utils' +import { ClusterExtensionAccordion } from 'components/PostgresClusterForm/PostgresClusterSteps' +import { useCloudProvider } from 'hooks/useCloudProvider' interface PostgresClusterProps extends DbLabInstanceFormProps { classes: ClassesType @@ -56,128 +60,18 @@ interface PostgresClusterProps extends DbLabInstanceFormProps { const PostgresCluster = (props: PostgresClusterProps) => { const { classes, orgPermissions } = props - const [state, dispatch] = useReducer(reducer, initialState) + const { + state, + dispatch, + handleChangeVolume, + handleSetFormStep, + handleReturnToForm, + } = useCloudProvider({ initialState, reducer }) const permitted = !orgPermissions || orgPermissions.dblabInstanceCreate - const urlParams = new URLSearchParams(window.location.search) - const urlTaskID = urlParams.get('taskID') - const urlProvider = urlParams.get('provider') const requirePublicKeys = !state.publicKeys && (state.provider === 'aws' || state.provider === 'gcp') - useEffect(() => { - if (urlTaskID && urlProvider) { - dispatch({ - type: 'set_form_step', - formStep: 'simple', - taskID: urlTaskID, - provider: urlProvider, - }) - } else { - dispatch({ - type: 'set_form_step', - formStep: initialState.formStep, - provider: initialState.provider, - }) - } - }, [urlTaskID, urlProvider]) - - useEffect(() => { - const fetchCloudDetails = async () => { - dispatch({ type: 'set_is_loading', isLoading: true }) - try { - const cloudRegions = await getCloudRegions(initialState.provider) - const cloudVolumes = await getCloudVolumes(initialState.provider) - const serviceProviders = await getCloudProviders() - const ssdCloudVolumes = cloudVolumes.response.filter( - (volume: CloudVolumes) => volume.api_name === initialState?.api_name, - )[0] - - dispatch({ - type: 'set_initial_state', - cloudRegions: cloudRegions.response, - volumes: cloudVolumes.response, - volumeType: `${ssdCloudVolumes.api_name} (${ssdCloudVolumes.cloud_provider}: ${ssdCloudVolumes.native_name})`, - volumeCurrency: ssdCloudVolumes.native_reference_price_currency, - volumePricePerHour: - ssdCloudVolumes.native_reference_price_per_1000gib_per_hour, - volumePrice: - (initialState.storage * - ssdCloudVolumes.native_reference_price_per_1000gib_per_hour) / - 1000, - serviceProviders: serviceProviders.response, - isLoading: false, - }) - } catch (error) { - console.log(error) - } - } - fetchCloudDetails() - }, []) - - useEffect(() => { - const fetchUpdatedDetails = async () => { - try { - const cloudRegions = await getCloudRegions(state.provider) - const cloudVolumes = await getCloudVolumes(state.provider) - const ssdCloudVolumes = cloudVolumes.response.filter( - (volume: CloudVolumes) => volume.api_name === initialState?.api_name, - )[0] - - dispatch({ - type: 'update_initial_state', - volumes: cloudVolumes.response, - volumeType: `${ssdCloudVolumes.api_name} (${ssdCloudVolumes.cloud_provider}: ${ssdCloudVolumes.native_name})`, - volumeCurrency: ssdCloudVolumes.native_reference_price_currency, - volumePricePerHour: - ssdCloudVolumes.native_reference_price_per_1000gib_per_hour, - volumePrice: - (initialState.storage * - ssdCloudVolumes.native_reference_price_per_1000gib_per_hour) / - 1000, - cloudRegions: cloudRegions.response, - }) - } catch (error) { - console.log(error) - } - } - fetchUpdatedDetails() - }, [state.api_name, state.provider]) - - useEffect(() => { - const fetchUpdatedDetails = async () => { - dispatch({ type: 'set_is_reloading', isReloading: true }) - try { - const cloudInstances = await getCloudInstances({ - provider: state.provider, - region: state.location.native_code, - }) - - dispatch({ - type: 'update_instance_type', - cloudInstances: cloudInstances.response, - instanceType: cloudInstances.response[0], - isReloading: false, - }) - } catch (error) { - console.log(error) - } - } - fetchUpdatedDetails() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.location.native_code]) - - const uniqueRegionsByProvider = state.cloudRegions - .map((region: CloudRegion) => region.world_part) - .filter( - (value: string, index: number, self: string) => - self.indexOf(value) === index, - ) - - const filteredRegions = state.cloudRegions.filter( - (region: CloudRegion) => region.world_part === state.region, - ) - const pageTitle = const breadcrumbs = ( { /> ) - const handleChangeVolume = ( - event: React.ChangeEvent, - ) => { - const volumeApiName = event.target.value.split(' ')[0] - const selectedVolume = state.volumes.filter( - (volume: CloudVolumes) => volume.api_name === volumeApiName, - )[0] - - dispatch({ - type: 'change_volume_type', - volumeType: event.target.value, - volumePricePerHour: - selectedVolume.native_reference_price_per_1000gib_per_hour, - volumePrice: - (state.storage * - selectedVolume.native_reference_price_per_1000gib_per_hour) / - 1000, - }) - } - - const handleSetFormStep = (step: string) => { - dispatch({ type: 'set_form_step', formStep: step }) - } - const handleReturnToList = () => { props.history.push(urls.linkClusters(props)) } - const handleReturnToForm = () => { - dispatch({ type: 'set_form_step', formStep: initialState.formStep }) - } - const checkSyncStandbyCount = () => { if (state.synchronous_mode) { if (Number(state.numberOfInstances) === 1) { @@ -281,6 +147,7 @@ const PostgresCluster = (props: PostgresClusterProps) => { dispatch({ type: 'change_provider', provider: provider.api_name, + isReloading: true, }) } > @@ -312,7 +179,7 @@ const PostgresCluster = (props: PostgresClusterProps) => { }) } > - {uniqueRegionsByProvider.map( + {uniqueRegionsByProvider(state.cloudRegions).map( (region: string, index: number) => ( {
- {filteredRegions.map((region: CloudRegion, index: number) => ( -
- dispatch({ - type: 'change_location', - location: region, - }) - } - > -

{region.api_name}

-

🏴 {region.label}

-
- ))} + {filteredRegions(state.cloudRegions, state.region).map( + (region: CloudRegion, index: number) => ( +
+ dispatch({ + type: 'change_location', + location: region, + }) + } + > +

{region.api_name}

+

🏴 {region.label}

+
+ ), + )}
{state.instanceType ? ( <> @@ -483,6 +352,34 @@ const PostgresCluster = (props: PostgresClusterProps) => {

5. Database volume

+ + + dispatch({ + type: 'change_file_system', + fileSystem: e.target.value, + }) + } + select + label="Filesystem" + InputLabelProps={{ + shrink: true, + }} + variant="outlined" + className={classes.filterSelect} + > + {state.fileSystemArray.map( + (p: string, id: number) => { + return ( + + {p} + + ) + }, + )} + + { }) } /> -

- 8. Provide SSH public keys (one per line) -

-

- These SSH public keys will be added to the DBLab server's -   - ~/.ssh/authorized_keys -   file. Providing at least one public key is - recommended to ensure access to the server after deployment. -

- , - ) => - dispatch({ - type: 'change_public_keys', - publicKeys: event.target.value, - }) - } + key !== 'region'), + )} + classes={classes} + dispatch={dispatch} /> {
+

+ 10. Provide SSH public keys (one per line) +

+

+ These SSH public keys will be added to the DBLab server's +   + ~/.ssh/authorized_keys +   file. Providing at least one public key is + recommended to ensure access to the server after deployment. +

+ , + ) => + dispatch({ + type: 'change_public_keys', + publicKeys: event.target.value, + }) + } + /> ) : (

@@ -857,7 +762,7 @@ const PostgresCluster = (props: PostgresClusterProps) => { handleSetFormStep('simple')} /> @@ -865,7 +770,7 @@ const PostgresCluster = (props: PostgresClusterProps) => { ) : state.formStep === 'ansible' && permitted ? ( { ) : state.formStep === 'docker' && permitted ? ( { ) : state.formStep === 'simple' && permitted ? ( +}) => ( + + + {step}. Extensions + + + + + dispatch({ + type: 'change_pg_repack', + pg_repack: e.target.checked, + }) + } + classes={{ + root: classes.checkboxRoot, + }} + /> + } + label={'pg_repack'} + /> + + dispatch({ + type: 'change_pg_cron', + pg_cron: e.target.checked, + }) + } + classes={{ + root: classes.checkboxRoot, + }} + /> + } + label={'pg_cron'} + /> + + dispatch({ + type: 'change_pgaudit', + pgaudit: e.target.checked, + }) + } + classes={{ + root: classes.checkboxRoot, + }} + /> + } + label={'pgaudit'} + /> + {state.version !== 10 && ( + + dispatch({ + type: 'change_pgvector', + pgvector: e.target.checked, + }) + } + classes={{ + root: classes.checkboxRoot, + }} + /> + } + label={'pgvector'} + /> + )} + + dispatch({ + type: 'change_postgis', + postgis: e.target.checked, + }) + } + classes={{ + root: classes.checkboxRoot, + }} + /> + } + label={'postgis'} + /> + + dispatch({ + type: 'change_pgrouting', + pgrouting: e.target.checked, + }) + } + classes={{ + root: classes.checkboxRoot, + }} + /> + } + label={'pgrouting'} + /> + {state.version !== 10 && state.version !== 11 && ( + + dispatch({ + type: 'change_timescaledb', + timescaledb: e.target.checked, + }) + } + classes={{ + root: classes.checkboxRoot, + }} + /> + } + label={'timescaledb'} + /> + )} + {state.version !== 10 && ( + + dispatch({ + type: 'change_citus', + citus: e.target.checked, + }) + } + classes={{ + root: classes.checkboxRoot, + }} + /> + } + label={'citus'} + /> + )} + + dispatch({ + type: 'change_pg_partman', + pg_partman: e.target.checked, + }) + } + classes={{ + root: classes.checkboxRoot, + }} + /> + } + label={'pg_partman'} + /> + + dispatch({ + type: 'change_pg_stat_kcache', + pg_stat_kcache: e.target.checked, + }) + } + classes={{ + root: classes.checkboxRoot, + }} + /> + } + label={'pg_stat_kcache'} + /> + + dispatch({ + type: 'change_pg_wait_sampling', + pg_wait_sampling: e.target.checked, + }) + } + classes={{ + root: classes.checkboxRoot, + }} + /> + } + label={'pg_wait_sampling'} + /> + + + +) diff --git a/ui/packages/platform/src/components/PostgresClusterForm/PostgresClusterWrapper.tsx b/ui/packages/platform/src/components/PostgresClusterForm/PostgresClusterWrapper.tsx index 44ac7db2..8c720e24 100644 --- a/ui/packages/platform/src/components/PostgresClusterForm/PostgresClusterWrapper.tsx +++ b/ui/packages/platform/src/components/PostgresClusterForm/PostgresClusterWrapper.tsx @@ -5,26 +5,22 @@ *-------------------------------------------------------------------------- */ - import { RouteComponentProps } from 'react-router' - - +import { RouteComponentProps } from 'react-router' + import { useInstanceFormStyles } from 'components/DbLabInstanceForm/DbLabInstanceFormWrapper' import PostgresCluster from './PostgresCluster' - - export interface PostgresClusterWrapperProps { - edit?: boolean - orgId: number - project: string | undefined - history: RouteComponentProps['history'] - orgPermissions: { - dblabInstanceCreate?: boolean - } - } - - export const PostgresClusterWrapper = (props: PostgresClusterWrapperProps) => { - - const classes = useInstanceFormStyles() - - return - } - \ No newline at end of file +import { OrgPermissions } from 'components/types' + +export interface PostgresClusterWrapperProps { + edit?: boolean + orgId: number + project: string | undefined + history: RouteComponentProps['history'] + orgPermissions: OrgPermissions +} + +export const PostgresClusterWrapper = (props: PostgresClusterWrapperProps) => { + const classes = useInstanceFormStyles() + + return +} diff --git a/ui/packages/platform/src/components/PostgresClusterForm/reducer/index.tsx b/ui/packages/platform/src/components/PostgresClusterForm/reducer/index.tsx index 48b028d9..2b13bcf0 100644 --- a/ui/packages/platform/src/components/PostgresClusterForm/reducer/index.tsx +++ b/ui/packages/platform/src/components/PostgresClusterForm/reducer/index.tsx @@ -7,9 +7,11 @@ import { ReducerAction } from 'react' -import { CloudRegion } from 'api/cloud/getCloudRegions' import { CloudInstance } from 'api/cloud/getCloudInstances' +import { CloudRegion } from 'api/cloud/getCloudRegions' import { CloudVolumes } from 'api/cloud/getCloudVolumes' +import { clusterExtensionsState } from 'components/PostgresClusterInstallForm/reducer' +import { useCloudProviderProps } from 'hooks/useCloudProvider' export const initialState = { isLoading: false, @@ -18,7 +20,7 @@ export const initialState = { provider: 'aws', storage: 100, region: 'North America', - version: '16', + version: 16, serviceProviders: [], cloudRegions: [], cloudInstances: [], @@ -45,10 +47,13 @@ export const initialState = { synchronous_node_count: 1, netdata_install: true, taskID: '', + fileSystem: 'zfs', + fileSystemArray: ['zfs', 'ext4', 'xfs'], + ...clusterExtensionsState, } export const reducer = ( - state: typeof initialState, + state: useCloudProviderProps['initialState'], // @ts-ignore action: ReducerAction, ) => { @@ -113,19 +118,7 @@ export const reducer = ( location: action.location, } } - case 'change_plan': { - return { - ...state, - plan: action.plan, - size: action.size, - } - } - case 'change_size': { - return { - ...state, - size: action.size, - } - } + case 'change_name': { return { ...state, @@ -166,18 +159,7 @@ export const reducer = ( storage: action.volumeSize, } } - case 'set_is_loading': { - return { - ...state, - isLoading: action.isLoading, - } - } - case 'set_is_reloading': { - return { - ...state, - isReloading: action.isReloading, - } - } + case 'set_form_step': { return { ...state, @@ -198,7 +180,7 @@ export const reducer = ( database_public_access: action.database_public_access, } } - + case 'change_with_haproxy_load_balancing': { return { ...state, @@ -233,7 +215,81 @@ export const reducer = ( netdata_install: action.netdata_install, } } - + + case 'change_pg_repack': { + return { + ...state, + pg_repack: action.pg_repack, + } + } + case 'change_pg_cron': { + return { + ...state, + pg_cron: action.pg_cron, + } + } + case 'change_pgaudit': { + return { + ...state, + pgaudit: action.pgaudit, + } + } + case 'change_pgvector': { + return { + ...state, + pgvector: action.pgvector, + } + } + case 'change_postgis': { + return { + ...state, + postgis: action.postgis, + } + } + case 'change_pgrouting': { + return { + ...state, + pgrouting: action.pgrouting, + } + } + case 'change_timescaledb': { + return { + ...state, + timescaledb: action.timescaledb, + } + } + case 'change_citus': { + return { + ...state, + citus: action.citus, + } + } + case 'change_pg_partman': { + return { + ...state, + pg_partman: action.pg_partman, + } + } + case 'change_pg_stat_kcache': { + return { + ...state, + pg_stat_kcache: action.pg_stat_kcache, + } + } + case 'change_pg_wait_sampling': { + return { + ...state, + pg_wait_sampling: action.pg_wait_sampling, + } + } + + case 'change_file_system': { + return { + ...state, + fileSystem: action.fileSystem, + } + } + default: throw new Error() } diff --git a/ui/packages/platform/src/components/PostgresClusterForm/utils/index.tsx b/ui/packages/platform/src/components/PostgresClusterForm/utils/index.tsx index e9e977ed..98bc1504 100644 --- a/ui/packages/platform/src/components/PostgresClusterForm/utils/index.tsx +++ b/ui/packages/platform/src/components/PostgresClusterForm/utils/index.tsx @@ -1,44 +1,19 @@ +import { CloudImage } from 'api/cloud/getCloudImages'; import { DEBUG_API_SERVER, dockerRunCommand } from "components/DbLabInstanceForm/utils"; -import { CloudImage } from 'api/cloud/getCloudImages' -import { initialState } from "components/DbLabInstanceForm/reducer"; +import { addIndentForAnsible, addIndentForDocker, clusterExtensions } from "components/PostgresClusterInstallForm/utils"; +import { useCloudProviderProps } from "hooks/useCloudProvider"; const API_SERVER = process.env.REACT_APP_API_SERVER const isDebugServer = API_SERVER === DEBUG_API_SERVER export const getClusterPlaybookCommand = ( - state: typeof initialState, - cloudImages: CloudImage, - orgKey: string, - ) => - `${dockerRunCommand(state.provider)} \\\r - vitabaks/postgresql_cluster:cloud \\\r - ansible-playbook deploy_pgcluster.yml --extra-vars \\\r - "ansible_user='${state.provider === "aws" ? 'ubuntu' : 'root'}' \\\r - provision='${state.provider}' \\\r - servers_count='${state.numberOfInstances}' \\\r - server_type='${state.instanceType.native_name}' \\\r - server_image='${cloudImages?.native_os_image}' \\\r - server_location='${state.location.native_code}' \\\r - volume_size='${state.storage}' \\\r - postgresql_version='${state.version}' \\\r - database_public_access='${state.database_public_access}' \\\r - with_haproxy_load_balancing='${state.with_haproxy_load_balancing}' \\\r - pgbouncer_install='${state.pgbouncer_install}' \\\r - synchronous_mode='${state.synchronous_mode}' \\\r - ${state.synchronous_mode ? `synchronous_node_count='${state.synchronous_node_count}' \\\r` : ``} - netdata_install='${state.netdata_install}' \\\r - patroni_cluster_name='${state.name}' \\\r - ${state.publicKeys ? `ssh_public_keys='${state.publicKeys}' \\\r` : ``} - ${ orgKey ? `platform_org_key='${orgKey}'${isDebugServer ? `\\\r` : `"`}` : `` } - ${ isDebugServer ? `platform_url='${DEBUG_API_SERVER}'"` : `` }` - -export const getClusterPlaybookCommandWithoutDocker = ( - state: typeof initialState, + state: useCloudProviderProps["initialState"], cloudImages: CloudImage, orgKey: string, -) => - `ansible-playbook deploy_pgcluster.yml --extra-vars \\\r + isDocker?: boolean, +) => { + const playbookVariables = `ansible-playbook deploy_pgcluster.yml --extra-vars \\\r "ansible_user='${state.provider === "aws" ? 'ubuntu' : 'root'}' \\\r provision='${state.provider}' \\\r servers_count='${state.numberOfInstances}' \\\r @@ -50,16 +25,25 @@ export const getClusterPlaybookCommandWithoutDocker = ( database_public_access='${state.database_public_access}' \\\r with_haproxy_load_balancing='${state.with_haproxy_load_balancing}' \\\r pgbouncer_install='${state.pgbouncer_install}' \\\r + pg_data_mount_fstype='${state.fileSystem}' \\\r synchronous_mode='${state.synchronous_mode}' \\\r ${state.synchronous_mode ? `synchronous_node_count='${state.synchronous_node_count}' \\\r` : ``} netdata_install='${state.netdata_install}' \\\r patroni_cluster_name='${state.name}' \\\r + ${addIndentForAnsible(clusterExtensions(state as unknown as {[key: string]: boolean | string | number} ))} ${state.publicKeys ? `ssh_public_keys='${state.publicKeys}' \\\r` : ``} - ${ orgKey ? `platform_org_key='${orgKey}'${isDebugServer ? `\\\r` : `"`}` : ``} - ${ isDebugServer ? `platform_url='${DEBUG_API_SERVER}'"` : `` }` + ${orgKey ? `platform_org_key='${orgKey}'${isDebugServer ? `\\\r` : `"`}` : ``} + ${isDebugServer ? `platform_url='${DEBUG_API_SERVER}'"` : ``}` + + if (isDocker) { + return `${dockerRunCommand(state.provider)} \\\r + vitabaks/postgresql_cluster:cloud \\\r + ${addIndentForDocker(playbookVariables)}` + } + return playbookVariables +} export const cloneClusterRepositoryCommand = () => -`git clone --depth 1 --branch cloud \\\r + `git clone --depth 1 --branch cloud \\\r https://github.com/vitabaks/postgresql_cluster.git \\\r && cd postgresql_cluster/` - \ No newline at end of file diff --git a/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterInstallForm.tsx b/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterInstallForm.tsx index 3b8a007e..62ba81a2 100644 --- a/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterInstallForm.tsx +++ b/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterInstallForm.tsx @@ -28,6 +28,7 @@ import { import { PostgresClusterInstallFormSidebar } from 'components/PostgresClusterInstallForm/PostgresClusterInstallFormSidebar' import { AnsibleInstance } from 'components/PostgresClusterInstallForm/PostgresClusterSteps/AnsibleInstance' import { DockerInstance } from 'components/PostgresClusterInstallForm/PostgresClusterSteps/DockerInstance' +import { ClusterExtensionAccordion } from 'components/PostgresClusterForm/PostgresClusterSteps' import { Select } from '@postgres.ai/shared/components/Select' import { validateDLEName } from 'utils/utils' @@ -159,13 +160,23 @@ const PostgresClusterInstallForm = (props: PostgresClusterInstallFormProps) => { }) } /> + key !== 'postgresql_data_dir', + ), + )} + classes={classes} + dispatch={dispatch} + /> - 4. Advanced options + 5. Advanced options void disabled: boolean }) => { diff --git a/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterInstallWrapper.tsx b/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterInstallWrapper.tsx index 91c7bed7..5e6bea25 100644 --- a/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterInstallWrapper.tsx +++ b/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterInstallWrapper.tsx @@ -9,15 +9,14 @@ import { RouteComponentProps } from 'react-router' import PostgresClusterInstallForm from './PostgresClusterInstallForm' import { useInstanceFormStyles } from 'components/DbLabInstanceForm/DbLabInstanceFormWrapper' +import { OrgPermissions } from 'components/types' export interface PostgresClusterInstallFormWrapperProps { edit?: boolean orgId: number project: string | undefined history: RouteComponentProps['history'] - orgPermissions: { - dblabInstanceCreate?: boolean - } + orgPermissions: OrgPermissions } export const PostgresClusterInstallWrapper = ( diff --git a/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterSteps/AnsibleInstance.tsx b/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterSteps/AnsibleInstance.tsx index 51313499..b02250c1 100644 --- a/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterSteps/AnsibleInstance.tsx +++ b/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterSteps/AnsibleInstance.tsx @@ -1,22 +1,22 @@ -import { Box } from '@mui/material' import { - Button, Accordion, - AccordionSummary, AccordionDetails, + AccordionSummary, + Button, } from '@material-ui/core' +import { Box } from '@mui/material' import { SyntaxHighlight } from '@postgres.ai/shared/components/SyntaxHighlight' +import { icons } from '@postgres.ai/shared/styles/icons' import { formStyles } from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' import { InstanceFormCreation } from 'components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation' -import { initialState } from '../reducer' +import { getAnsibleInstallationCommand } from 'components/DbLabInstanceInstallForm/utils' +import { useCloudProviderProps } from 'hooks/useCloudProvider' import { - getAnsibleClusterCommand, + getClusterCommand, getClusterExampleCommand, getPostgresClusterInstallationCommand, } from '../utils' -import { getAnsibleInstallationCommand } from 'components/DbLabInstanceInstallForm/utils' -import { icons } from '@postgres.ai/shared/styles/icons' export const AnsibleInstance = ({ state, @@ -25,7 +25,7 @@ export const AnsibleInstance = ({ formStep, setFormStep, }: { - state: typeof initialState + state: useCloudProviderProps['initialState'] goBack: () => void goBackToForm: () => void formStep: string @@ -108,7 +108,7 @@ export const AnsibleInstance = ({

5. Execute the Ansible playbook to Postgres Cluster

- +

6. After the code snippet runs successfully, follow the directions displayed in the resulting output to start using the database. diff --git a/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterSteps/DockerInstance.tsx b/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterSteps/DockerInstance.tsx index 78affe10..97c1c0ae 100644 --- a/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterSteps/DockerInstance.tsx +++ b/ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterSteps/DockerInstance.tsx @@ -1,21 +1,21 @@ -import { Box } from '@mui/material' import { - Button, Accordion, - AccordionSummary, AccordionDetails, + AccordionSummary, + Button, } from '@material-ui/core' +import { Box } from '@mui/material' import { SyntaxHighlight } from '@postgres.ai/shared/components/SyntaxHighlight' +import { icons } from '@postgres.ai/shared/styles/icons' import { formStyles } from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' import { InstanceFormCreation } from 'components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation' -import { initialState } from '../reducer' +import { useCloudProviderProps } from 'hooks/useCloudProvider' import { getClusterCommand, getClusterExampleCommand, getInventoryPreparationCommand, } from '../utils' -import { icons } from '@postgres.ai/shared/styles/icons' export const DockerInstance = ({ state, @@ -24,7 +24,7 @@ export const DockerInstance = ({ formStep, setFormStep, }: { - state: typeof initialState + state: useCloudProviderProps['initialState'] goBack: () => void goBackToForm: () => void formStep: string @@ -88,7 +88,7 @@ export const DockerInstance = ({

3. Run ansible playbook to deploy Postgres Cluster

- +

4. After the code snippet runs successfully, follow the directions displayed in the resulting output to start using the database. diff --git a/ui/packages/platform/src/components/PostgresClusterInstallForm/reducer/index.tsx b/ui/packages/platform/src/components/PostgresClusterInstallForm/reducer/index.tsx index f6c51e6c..79a82c8e 100644 --- a/ui/packages/platform/src/components/PostgresClusterInstallForm/reducer/index.tsx +++ b/ui/packages/platform/src/components/PostgresClusterInstallForm/reducer/index.tsx @@ -7,12 +7,26 @@ import { ReducerAction } from 'react' +export const clusterExtensionsState = { + pg_repack: true, + pg_cron: true, + pgaudit: true, + pgvector: true, + postgis: false, + pgrouting: false, + timescaledb: false, + citus: false, + pg_partman: false, + pg_stat_kcache: false, + pg_wait_sampling: false, +} + export const initialState = { isLoading: false, isReloading: false, formStep: 'create', patroni_cluster_name: '', - version: '16', + version: 16, postgresql_data_dir: '/var/lib/postgresql/16/', cluster_vip: '', with_haproxy_load_balancing: false, @@ -21,6 +35,7 @@ export const initialState = { synchronous_node_count: 1, netdata_install: true, taskID: '', + ...clusterExtensionsState, } export const reducer = ( @@ -98,22 +113,76 @@ export const reducer = ( } } - case 'set_is_loading': { + case 'set_form_step': { return { ...state, - isLoading: action.isLoading, + formStep: action.formStep, } } - case 'set_is_reloading': { + case 'change_pg_repack': { return { ...state, - isReloading: action.isReloading, + pg_repack: action.pg_repack, } } - case 'set_form_step': { + case 'change_pg_cron': { return { ...state, - formStep: action.formStep, + pg_cron: action.pg_cron, + } + } + case 'change_pgaudit': { + return { + ...state, + pgaudit: action.pgaudit, + } + } + case 'change_pgvector': { + return { + ...state, + pgvector: action.pgvector, + } + } + case 'change_postgis': { + return { + ...state, + postgis: action.postgis, + } + } + case 'change_pgrouting': { + return { + ...state, + pgrouting: action.pgrouting, + } + } + case 'change_timescaledb': { + return { + ...state, + timescaledb: action.timescaledb, + } + } + case 'change_citus': { + return { + ...state, + citus: action.citus, + } + } + case 'change_pg_partman': { + return { + ...state, + pg_partman: action.pg_partman, + } + } + case 'change_pg_stat_kcache': { + return { + ...state, + pg_stat_kcache: action.pg_stat_kcache, + } + } + case 'change_pg_wait_sampling': { + return { + ...state, + pg_wait_sampling: action.pg_wait_sampling, } } default: diff --git a/ui/packages/platform/src/components/PostgresClusterInstallForm/utils/index.tsx b/ui/packages/platform/src/components/PostgresClusterInstallForm/utils/index.tsx index 1f6902cb..1235f11c 100644 --- a/ui/packages/platform/src/components/PostgresClusterInstallForm/utils/index.tsx +++ b/ui/packages/platform/src/components/PostgresClusterInstallForm/utils/index.tsx @@ -1,10 +1,34 @@ -import { initialState } from "../reducer" +import { useCloudProviderProps } from "hooks/useCloudProvider" export const getInventoryPreparationCommand = () => `curl -fsSL https://raw.githubusercontent.com/vitabaks/postgresql_cluster/master/inventory \\\r --output ~/inventory nano ~/inventory` +export const addIndentForDocker = (command: string) => + command + .split('\r\n') + .map((line, index) => { + if (index === 0) { + return `${line}` + } else { + return `${' '.repeat(8)}${line.replace(/^\s+/, '')}` + } + }) + .join('\r\n') + +export const addIndentForAnsible = (command: string) => +command + .split('\r\n') + .map((line, index) => { + if (index === 0) { + return `${line.replace(/^\s+/, '')}` + } else { + return `${' '.repeat(2)}${line.replace(/^\s+/, '')}` + } + }) + .join('\r\n') + export const getClusterExampleCommand = () => `[etcd_cluster] 10.0.1.1 ansible_host=5.161.228.76 @@ -40,24 +64,44 @@ ansible_ssh_private_key_file=/root/.ssh/id_rsa [pgbackrest:vars] ` +export const clusterExtensions = (state: {[key: string]: boolean | string | number}) => `${state.pg_repack ? `enable_pg_repack='${state.pg_repack}' \\\r` : ''} +${state.pg_cron ? `enable_pg_cron='${state.pg_cron}' \\\r` : ''} +${state.pgaudit ? `enable_pgaudit='${state.pgaudit}' \\\r` : ''} +${state.version !== 10 && state.pgvector ? `enable_pgvector='${state.pgvector}' \\\r` : ''} +${state.postgis ? `enable_postgis='${state.postgis}' \\\r` : ''} +${state.pgrouting ? `enable_pgrouting='${state.pgrouting}' \\\r` : ''} +${state.version !== 10 && state.version !== 11 && state.timescaledb ? `enable_timescaledb='${state.timescaledb}' \\\r` : ''} +${state.version !== 10 && state.citus ? `enable_citus='${state.citus}' \\\r` : ''} +${state.pg_partman ? `enable_pg_partman='${state.pg_partman}' \\\r` : ''} +${state.pg_stat_kcache ? `enable_pg_stat_kcache='${state.pg_stat_kcache}' \\\r` : ''} +${state.pg_wait_sampling ? `enable_pg_wait_sampling='${state.pg_wait_sampling}' \\\r` : ''}` + export const getClusterCommand = ( - state: typeof initialState, - ) => - `docker run --rm -it \\\r + state: useCloudProviderProps['initialState'], + isDocker?: boolean, +) => { + const playbookVariables = `ansible-playbook deploy_pgcluster.yml --extra-vars \\\r + "postgresql_version='${state.version}' \\\r + patroni_cluster_name='${state.patroni_cluster_name}' \\\r + ${addIndentForAnsible(clusterExtensions(state))} + ${state.cluster_vip ? `cluster_vip='${state.cluster_vip} \\\r'` : ''} + with_haproxy_load_balancing='${state.with_haproxy_load_balancing}' \\\r + pgbouncer_install='${state.pgbouncer_install}' \\\r + synchronous_mode='${state.synchronous_mode}' \\\r + ${state.synchronous_mode ? `synchronous_node_count='${state.synchronous_node_count}' \\\r` : ''} + netdata_install='${state.netdata_install}' \\\r + postgresql_data_dir='${state.postgresql_data_dir}'"` + + if (isDocker) { + return `docker run --rm -it \\\r -v ~/inventory:/postgresql_cluster/inventory:ro \\\r -v $HOME/.ssh:/root/.ssh:ro -e ANSIBLE_SSH_ARGS="-F none" \\\r vitabaks/postgresql_cluster:cloud \\\r - ansible-playbook deploy_pgcluster.yml --extra-vars \\\r - "postgresql_version='${state.version}' \\\r - patroni_cluster_name='${state.patroni_cluster_name}' \\\r - ${state.cluster_vip ? `cluster_vip='${state.cluster_vip} \\\r'` : ''} - with_haproxy_load_balancing='${state.with_haproxy_load_balancing}' \\\r - pgbouncer_install='${state.pgbouncer_install}' \\\r - synchronous_mode='${state.synchronous_mode}' \\\r - ${state.synchronous_mode ? `synchronous_node_count='${state.synchronous_node_count}' \\\r` : ''} - netdata_install='${state.netdata_install}' \\\r - postgresql_data_dir='${state.postgresql_data_dir}'" -` + ${addIndentForDocker(playbookVariables)}` + } + + return playbookVariables +} export const getPostgresClusterInstallationCommand = () => `git clone --depth 1 --branch cloud \\\r @@ -65,21 +109,6 @@ export const getPostgresClusterInstallationCommand = () => && cd postgresql_cluster/ ` -export const getAnsibleClusterCommand = ( - state: typeof initialState, - ) => - `ansible-playbook deploy_pgcluster.yml --extra-vars \\\r - "postgresql_version='${state.version}' \\\r - patroni_cluster_name='${state.patroni_cluster_name}' \\\r - ${state.cluster_vip ? `cluster_vip='${state.cluster_vip} \\\r'` : ''} - with_haproxy_load_balancing='${state.with_haproxy_load_balancing}' \\\r - pgbouncer_install='${state.pgbouncer_install}' \\\r - synchronous_mode='${state.synchronous_mode}' \\\r - ${state.synchronous_mode ? `synchronous_node_count='${state.synchronous_node_count}' \\\r` : ''} - netdata_install='${state.netdata_install}' \\\r - postgresql_data_dir='${state.postgresql_data_dir}'" -` - export function isIPAddress(input: string) { if (input === '') { return true diff --git a/ui/packages/platform/src/components/PostgresClusters/PostgresClusters.tsx b/ui/packages/platform/src/components/PostgresClusters/PostgresClusters.tsx index 66a2be56..fb361911 100644 --- a/ui/packages/platform/src/components/PostgresClusters/PostgresClusters.tsx +++ b/ui/packages/platform/src/components/PostgresClusters/PostgresClusters.tsx @@ -5,52 +5,42 @@ *-------------------------------------------------------------------------- */ -import { Component, MouseEvent } from 'react' -import { formatDistanceToNowStrict } from 'date-fns' import { + Menu, + MenuItem, Table, TableBody, TableCell, TableHead, TableRow, TextField, - IconButton, - Menu, - MenuItem, - Tooltip, } from '@material-ui/core' -import MoreVertIcon from '@material-ui/icons/MoreVert' -import WarningIcon from '@material-ui/icons/Warning' +import { Component, MouseEvent } from 'react' -import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer' -import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner' -import { Spinner } from '@postgres.ai/shared/components/Spinner' -import { Modal } from '@postgres.ai/shared/components/Modal' -import { styles } from '@postgres.ai/shared/styles/styles' import { ClassesType, ProjectProps, RefluxTypes, } from '@postgres.ai/platform/src/components/types' +import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer' +import { Modal } from '@postgres.ai/shared/components/Modal' +import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner' +import { styles } from '@postgres.ai/shared/styles/styles' -import Actions from '../../actions/actions' -import ConsolePageTitle from './../ConsolePageTitle' +import { InstanceDto } from '@postgres.ai/shared/types/api/entities/instance' +import { InstanceStateDto } from '@postgres.ai/shared/types/api/entities/instanceState' +import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' +import { ConsoleButtonWrapper } from 'components/ConsoleButton/ConsoleButtonWrapper' +import { CreateClusterCards } from 'components/CreateClusterCards/CreateClusterCards' +import { DbLabInstancesProps } from 'components/DbLabInstances/DbLabInstancesWrapper' import { ErrorWrapper } from 'components/Error/ErrorWrapper' +import { WarningWrapper } from 'components/Warning/WarningWrapper' +import { ProjectDataType, getProjectAliasById } from 'utils/aliases' +import Actions from '../../actions/actions' import { messages } from '../../assets/messages' import Store from '../../stores/store' import Urls from '../../utils/urls' -import format from '../../utils/format' -import { isHttps } from '../../utils/utils' -import { WarningWrapper } from 'components/Warning/WarningWrapper' -import { ProjectDataType, getProjectAliasById } from 'utils/aliases' -import { InstanceStateDto } from '@postgres.ai/shared/types/api/entities/instanceState' -import { InstanceDto } from '@postgres.ai/shared/types/api/entities/instance' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { DbLabStatusWrapper } from 'components/DbLabStatus/DbLabStatusWrapper' -import { DbLabInstancesProps } from 'components/DbLabInstances/DbLabInstancesWrapper' -import { CreatedDbLabCards } from 'components/CreateDbLabCards/CreateDbLabCards' -import { CreateClusterCards } from 'components/CreateClusterCards/CreateClusterCards' -import { ConsoleButtonWrapper } from 'components/ConsoleButton/ConsoleButtonWrapper' +import ConsolePageTitle from './../ConsolePageTitle' interface PostgresClustersProps extends DbLabInstancesProps { classes: ClassesType @@ -293,19 +283,6 @@ class PostgresClusters extends Component< const deletePermitted = !orgPermissions || orgPermissions.dblabInstanceDelete - const getVersionDigits = (str: string) => { - if (!str) { - return 'N/A' - } - - const digits = str.match(/\d+/g) - - if (digits && digits.length > 0) { - return `${digits[0]}.${digits[1]}.${digits[2]}` - } - return '' - } - const addInstanceButton = (

- + ) diff --git a/ui/packages/platform/src/components/PostgresClusters/PostgresClustersWrapper.tsx b/ui/packages/platform/src/components/PostgresClusters/PostgresClustersWrapper.tsx index b0891106..510e4ee5 100644 --- a/ui/packages/platform/src/components/PostgresClusters/PostgresClustersWrapper.tsx +++ b/ui/packages/platform/src/components/PostgresClusters/PostgresClustersWrapper.tsx @@ -3,6 +3,7 @@ import { styles } from '@postgres.ai/shared/styles/styles' import { RouteComponentProps } from 'react-router' import { colors } from '@postgres.ai/shared/styles/colors' import PostgresClusters from './PostgresClusters' +import { OrgPermissions } from 'components/types' export interface DbLabInstancesProps { orgId: number @@ -17,11 +18,7 @@ export interface DbLabInstancesProps { org?: string } } - orgPermissions: { - dblabInstanceCreate?: boolean - dblabInstanceDelete?: boolean - dblabInstanceList?: boolean - } + orgPermissions: OrgPermissions } export const PostgresClustersWrapper = (props: DbLabInstancesProps) => { diff --git a/ui/packages/platform/src/components/Reports/ReportsWrapper.tsx b/ui/packages/platform/src/components/Reports/ReportsWrapper.tsx index 19e542ce..84af32ec 100644 --- a/ui/packages/platform/src/components/Reports/ReportsWrapper.tsx +++ b/ui/packages/platform/src/components/Reports/ReportsWrapper.tsx @@ -2,16 +2,14 @@ import { makeStyles } from '@material-ui/core' import { styles } from '@postgres.ai/shared/styles/styles' import { RouteComponentProps } from 'react-router' import Reports from 'components/Reports/Reports' +import { OrgPermissions } from 'components/types' export interface ReportsProps extends RouteComponentProps { projectId: string | undefined | number project: string | undefined orgId: number org: string | number - orgPermissions: { - checkupReportConfigure?: boolean - checkupReportDelete?: boolean - } + orgPermissions: OrgPermissions } export const ReportsWrapper = (props: ReportsProps) => { diff --git a/ui/packages/platform/src/components/StripeForm/index.tsx b/ui/packages/platform/src/components/StripeForm/index.tsx index 00d60d52..5c5b31c5 100644 --- a/ui/packages/platform/src/components/StripeForm/index.tsx +++ b/ui/packages/platform/src/components/StripeForm/index.tsx @@ -4,20 +4,20 @@ * Unauthorized copying of this file, via any medium is strictly prohibited *-------------------------------------------------------------------------- */ +import { Button, Paper, Tooltip, makeStyles } from '@material-ui/core' import { Box } from '@mui/material' import { formatDistanceToNowStrict } from 'date-fns' -import { useReducer, useEffect, useCallback, ReducerAction } from 'react' -import { Button, makeStyles, Paper, Tooltip } from '@material-ui/core' +import { ReducerAction, useCallback, useEffect, useReducer } from 'react' -import { colors } from '@postgres.ai/shared/styles/colors' +import { Link } from '@postgres.ai/shared/components/Link2' import { Spinner } from '@postgres.ai/shared/components/Spinner' +import { colors } from '@postgres.ai/shared/styles/colors' import { stripeStyles } from 'components/StripeForm/stripeStyles' -import { Link } from '@postgres.ai/shared/components/Link2' import { ROUTES } from 'config/routes' import { getPaymentMethods } from 'api/billing/getPaymentMethods' -import { startBillingSession } from 'api/billing/startBillingSession' import { getSubscription } from 'api/billing/getSubscription' +import { startBillingSession } from 'api/billing/startBillingSession' import format from '../../utils/format' @@ -182,8 +182,8 @@ function StripeForm(props: { const initialState = { isLoading: false, isFetching: false, - cards: [], - billingInfo: [], + cards: [] as { id: string; card: { exp_year: string; exp_month: string; brand: string; last4: string } }[], + billingInfo: [] as BillingSubscription[], } const reducer = ( diff --git a/ui/packages/platform/src/hooks/useCloudProvider.ts b/ui/packages/platform/src/hooks/useCloudProvider.ts new file mode 100644 index 00000000..9227f533 --- /dev/null +++ b/ui/packages/platform/src/hooks/useCloudProvider.ts @@ -0,0 +1,183 @@ +import { Reducer, useCallback, useEffect, useReducer } from 'react' + +import { getCloudInstances } from 'api/cloud/getCloudInstances' +import { getCloudProviders } from 'api/cloud/getCloudProviders' +import { getCloudRegions } from 'api/cloud/getCloudRegions' +import { CloudVolumes, getCloudVolumes } from 'api/cloud/getCloudVolumes' +import { formatVolumeDetails } from 'components/DbLabInstanceForm/utils' +import { generateToken } from 'utils/utils' + +interface State { + [key: string]: StateValue +} + +type StateValue = + | string + | number + | boolean + | undefined + | unknown + | { [key: string]: string } + +export interface useCloudProviderProps { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initialState: any + reducer: Reducer +} + +export const useCloudProvider = ({ + initialState, + reducer, +}: useCloudProviderProps) => { + const [state, dispatch] = useReducer(reducer, initialState) + + const urlParams = new URLSearchParams(window.location.search) + const urlTaskID = urlParams.get('taskID') + const urlProvider = urlParams.get('provider') + + const fetchCloudData = useCallback( + async (provider: string) => { + const cloudRegions = await getCloudRegions(provider) + const cloudVolumes = await getCloudVolumes(provider) + const ssdCloudVolumes = cloudVolumes.response.find( + (volume: CloudVolumes) => volume.api_name === initialState?.api_name, + ) + + return { + cloudRegions: cloudRegions.response, + cloudVolumes: cloudVolumes.response, + ssdCloudVolume: ssdCloudVolumes, + } + }, + [initialState.api_name], + ) + + useEffect(() => { + const action = { + type: 'set_form_step', + formStep: urlTaskID && urlProvider ? 'simple' : initialState.formStep, + taskID: urlTaskID || undefined, + provider: urlProvider || initialState.provider, + } + + dispatch(action) + }, [urlTaskID, urlProvider, initialState.formStep, initialState.provider]) + + useEffect(() => { + const fetchInitialCloudDetails = async () => { + try { + const { cloudRegions, cloudVolumes, ssdCloudVolume } = + await fetchCloudData(initialState.provider as string) + const volumeDetails = formatVolumeDetails( + ssdCloudVolume, + initialState.storage as number, + ) + const serviceProviders = await getCloudProviders() + + dispatch({ + type: 'set_initial_state', + cloudRegions, + volumes: cloudVolumes, + ...volumeDetails, + serviceProviders: serviceProviders.response, + isLoading: false, + }) + } catch (error) { + console.error(error) + } + } + + fetchInitialCloudDetails() + }, [initialState.provider, initialState.storage, fetchCloudData]) + + useEffect(() => { + const fetchUpdatedCloudDetails = async () => { + try { + const { cloudRegions, cloudVolumes, ssdCloudVolume } = + await fetchCloudData(state.provider) + const volumeDetails = formatVolumeDetails( + ssdCloudVolume, + initialState.storage as number, + ) + + dispatch({ + type: 'update_initial_state', + cloudRegions, + volumes: cloudVolumes, + ...volumeDetails, + }) + } catch (error) { + console.error(error) + } + } + + fetchUpdatedCloudDetails() + }, [state.api_name, state.provider, initialState.storage, fetchCloudData]) + + useEffect(() => { + if (state.location.native_code && state.provider) { + const fetchUpdatedDetails = async () => { + try { + const cloudInstances = await getCloudInstances({ + provider: state.provider, + region: state.location.native_code, + }) + + dispatch({ + type: 'update_instance_type', + cloudInstances: cloudInstances.response, + instanceType: cloudInstances.response[0], + isReloading: false, + }) + } catch (error) { + console.log(error) + } + } + fetchUpdatedDetails() + } + }, [state.location.native_code, state.provider]) + + const handleReturnToForm = () => { + dispatch({ type: 'set_form_step', formStep: initialState.formStep }) + } + + const handleSetFormStep = (step: string) => { + dispatch({ type: 'set_form_step', formStep: step }) + } + + const handleGenerateToken = () => { + dispatch({ + type: 'change_verification_token', + verificationToken: generateToken(), + }) + } + + const handleChangeVolume = ( + event: React.ChangeEvent, + ) => { + const volumeApiName = event.target.value.split(' ')[0] + const selectedVolume = state.volumes.filter( + (volume: CloudVolumes) => volume.api_name === volumeApiName, + )[0] + + dispatch({ + type: 'change_volume_type', + volumeType: event.target.value, + volumePricePerHour: + selectedVolume.native_reference_price_per_1000gib_per_hour, + volumePrice: + (state.storage * + selectedVolume.native_reference_price_per_1000gib_per_hour) / + 1000, + }) + } + + return { + state, + dispatch, + handleReturnToForm, + handleSetFormStep, + handleGenerateToken, + handleChangeVolume, + } +} diff --git a/ui/packages/shared/icons/Renewable/index.tsx b/ui/packages/shared/icons/Renewable/index.tsx index 458123e1..0892789b 100644 --- a/ui/packages/shared/icons/Renewable/index.tsx +++ b/ui/packages/shared/icons/Renewable/index.tsx @@ -26,42 +26,36 @@ export const RenewableIcon = React.forwardRef( diff --git a/ui/packages/shared/icons/Shield/index.tsx b/ui/packages/shared/icons/Shield/index.tsx index 4544db96..2444ea39 100644 --- a/ui/packages/shared/icons/Shield/index.tsx +++ b/ui/packages/shared/icons/Shield/index.tsx @@ -24,7 +24,6 @@ export const ShieldIcon = React.forwardRef( xmlns="http://www.w3.org/2000/svg" > From a459675bdd4a16efc7643eaa37b3deb5faa5d694 Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Fri, 12 Jul 2024 15:52:21 +0000 Subject: [PATCH 043/111] Add tag 0.4.4 for extended-postgres image --- .../shared/pages/Configuration/utils/index.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ui/packages/shared/pages/Configuration/utils/index.ts b/ui/packages/shared/pages/Configuration/utils/index.ts index 21e97781..f671631e 100644 --- a/ui/packages/shared/pages/Configuration/utils/index.ts +++ b/ui/packages/shared/pages/Configuration/utils/index.ts @@ -7,14 +7,14 @@ const seContainerRegistry = 'se-images' const genericImagePrefix = 'postgresai/extended-postgres' // since some tags are rc, we need to specify the exact tags to use const dockerImagesConfig = { - '9.6': ['0.4.2', '0.4.1', '0.3.0'], - '10': ['0.4.2', '0.4.1', '0.3.0'], - '11': ['0.4.2', '0.4.1', '0.3.0'], - '12': ['0.4.2', '0.4.1', '0.3.0'], - '13': ['0.4.2', '0.4.1', '0.3.0'], - '14': ['0.4.2', '0.4.1', '0.3.0'], - '15': ['0.4.2', '0.4.1', '0.3.0'], - '16': ['0.4.2', '0.4.1'], + '9.6': ['0.4.4', '0.4.3'], + '10': ['0.4.4', '0.4.3'], + '11': ['0.4.4', '0.4.3'], + '12': ['0.4.4', '0.4.3'], + '13': ['0.4.4', '0.4.3'], + '14': ['0.4.4', '0.4.3'], + '15': ['0.4.4', '0.4.3'], + '16': ['0.4.4', '0.4.3'], } export type FormValuesKey = keyof FormValues From f055cd5750207a4a22f3b0bcaa8b9ba7442941f3 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Tue, 23 Jul 2024 18:50:01 +0000 Subject: [PATCH 044/111] Bot UI: fix: Show debug logs while creating a thread --- .../pages/Bot/DebugConsole/DebugConsole.tsx | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx b/ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx index 7e74a8fa..21a3bb57 100644 --- a/ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx +++ b/ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx @@ -65,23 +65,20 @@ export const DebugConsole = (props: DebugConsoleProps) => { const containerRef = useRef(null); useEffect(() => { - if (containerRef.current && debugMessages?.length && threadId && !debugMessagesLoading && isOpen) { - let code = containerRef.current.getElementsByTagName('code')?.[0]; - if (!code) { - code = document.createElement('code'); - containerRef.current.appendChild(code); - } + if (!containerRef.current || !debugMessages?.length || debugMessagesLoading || !isOpen) return; - if (code.hasChildNodes()) { - const lastMessage = debugMessages[debugMessages.length - 1]; - const fragment = createMessageFragment([lastMessage]); - code.appendChild(fragment); - } else { - const fragment = createMessageFragment(debugMessages); - code.appendChild(fragment); - } + let code = containerRef.current.getElementsByTagName('code')?.[0]; + if (!code) { + code = document.createElement('code'); + containerRef.current.appendChild(code); } - }, [debugMessages, isOpen, threadId, debugMessagesLoading]); + + const fragment = createMessageFragment( + code.hasChildNodes() ? [debugMessages[debugMessages.length - 1]] : debugMessages + ); + code.appendChild(fragment); + + }, [debugMessages, isOpen, debugMessagesLoading]); return ( Date: Tue, 23 Jul 2024 19:03:52 +0000 Subject: [PATCH 045/111] Bot UI: Convert thread and redirect to bot page --- ui/packages/platform/Dockerfile | 3 ++ ui/packages/platform/ci_docker_build_push.sh | 1 + ui/packages/platform/deploy/configs/dev.sh | 3 +- ui/packages/platform/deploy/configs/local.sh | 1 + .../platform/deploy/configs/production.sh | 3 +- .../platform/deploy/configs/staging.sh | 3 +- .../platform/deploy/platform-console.yaml | 2 ++ .../platform/src/api/bot/convertThread.ts | 29 +++++++++++++++++++ .../src/components/Dashboard/Dashboard.tsx | 21 ++++++++++++++ .../src/components/IndexPage/IndexPage.tsx | 7 +++-- 10 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 ui/packages/platform/src/api/bot/convertThread.ts diff --git a/ui/packages/platform/Dockerfile b/ui/packages/platform/Dockerfile index 774c840a..7367bfb0 100644 --- a/ui/packages/platform/Dockerfile +++ b/ui/packages/platform/Dockerfile @@ -47,6 +47,9 @@ ENV REACT_APP_SENTRY_DSN=$ARG_REACT_APP_SENTRY_DSN ARG ARG_REACT_APP_WS_URL ENV REACT_APP_WS_URL=$ARG_REACT_APP_WS_URL +ARG ARG_REACT_APP_BOT_API_URL +ENV REACT_APP_BOT_API_URL=$ARG_REACT_APP_BOT_API_URL + RUN apk add --no-cache --update git && \ npm i -g pnpm@7.30.5; \ pnpm config set store-dir /app/.pnpm-store; \ diff --git a/ui/packages/platform/ci_docker_build_push.sh b/ui/packages/platform/ci_docker_build_push.sh index 2380ac0f..b18aec0b 100644 --- a/ui/packages/platform/ci_docker_build_push.sh +++ b/ui/packages/platform/ci_docker_build_push.sh @@ -38,6 +38,7 @@ DOCKER_BUILDKIT=1 docker build \ --build-arg ARG_REACT_APP_ROOT_URL="${REACT_APP_ROOT_URL}" \ --build-arg ARG_REACT_APP_SENTRY_DSN="${REACT_APP_SENTRY_DSN}" \ --build-arg ARG_REACT_APP_WS_URL="${REACT_APP_WS_URL}" \ + --build-arg ARG_REACT_APP_BOT_API_URL="${REACT_APP_BOT_API_URL}" \ $tags_build --file ./ui/packages/platform/Dockerfile . set +x diff --git a/ui/packages/platform/deploy/configs/dev.sh b/ui/packages/platform/deploy/configs/dev.sh index 86eb03b4..91b90ca8 100644 --- a/ui/packages/platform/deploy/configs/dev.sh +++ b/ui/packages/platform/deploy/configs/dev.sh @@ -22,4 +22,5 @@ export REACT_APP_SENTRY_DSN="" # AI Bot # don't forget trailing slash if GET path is used! -export REACT_APP_WS_URL="wss://v2.postgres.ai/ai-bot-ws/" # we use staging for now, since dev env is not fullfledged yet \ No newline at end of file +export REACT_APP_WS_URL="wss://v2.postgres.ai/ai-bot-ws/" # we use staging for now, since dev env is not fullfledged yet +export REACT_APP_BOT_API_URL="https://v2.postgres.ai/ai-bot-api/bot" \ No newline at end of file diff --git a/ui/packages/platform/deploy/configs/local.sh b/ui/packages/platform/deploy/configs/local.sh index a55f6495..d8d687dc 100644 --- a/ui/packages/platform/deploy/configs/local.sh +++ b/ui/packages/platform/deploy/configs/local.sh @@ -22,3 +22,4 @@ export REACT_APP_SENTRY_DSN="" # AI Bot export REACT_APP_WS_URL="ws://localhost:9100/" # don't forget trailing slash if GET path is used! +export REACT_APP_BOT_API_URL="https://v2.postgres.ai/ai-bot-api/bot" \ No newline at end of file diff --git a/ui/packages/platform/deploy/configs/production.sh b/ui/packages/platform/deploy/configs/production.sh index 9a23b907..ba5426b9 100644 --- a/ui/packages/platform/deploy/configs/production.sh +++ b/ui/packages/platform/deploy/configs/production.sh @@ -23,4 +23,5 @@ export REACT_APP_STRIPE_API_KEY="xxx" export REACT_APP_SENTRY_DSN="https://91517477289e477cb8880f2f07a82632@sentry.postgres.ai/2" # AI Bot -export REACT_APP_WS_URL="wss://postgres.ai/ai-bot-ws/" # don't forget trailing slash! \ No newline at end of file +export REACT_APP_WS_URL="wss://postgres.ai/ai-bot-ws/" # don't forget trailing slash! +export REACT_APP_BOT_API_URL="https://postgres.ai/ai-bot-api/bot" \ No newline at end of file diff --git a/ui/packages/platform/deploy/configs/staging.sh b/ui/packages/platform/deploy/configs/staging.sh index 93b27a35..c1a250ab 100644 --- a/ui/packages/platform/deploy/configs/staging.sh +++ b/ui/packages/platform/deploy/configs/staging.sh @@ -21,4 +21,5 @@ export REACT_APP_STRIPE_API_KEY="xxx" export REACT_APP_SENTRY_DSN="" # AI Bot -export REACT_APP_WS_URL="wss://v2.postgres.ai/ai-bot-ws/" # don't forget trailing slash! \ No newline at end of file +export REACT_APP_WS_URL="wss://v2.postgres.ai/ai-bot-ws/" # don't forget trailing slash! +export REACT_APP_BOT_API_URL="https://v2.postgres.ai/ai-bot-api/bot" \ No newline at end of file diff --git a/ui/packages/platform/deploy/platform-console.yaml b/ui/packages/platform/deploy/platform-console.yaml index 10c5a48d..1db73300 100644 --- a/ui/packages/platform/deploy/platform-console.yaml +++ b/ui/packages/platform/deploy/platform-console.yaml @@ -49,3 +49,5 @@ spec: value: "$REACT_APP_STRIPE_API_KEY" - name: REACT_APP_WS_URL value: "$REACT_APP_WS_URL" + - name: REACT_APP_BOT_API_URL + value: "$REACT_APP_BOT_API_URL" diff --git a/ui/packages/platform/src/api/bot/convertThread.ts b/ui/packages/platform/src/api/bot/convertThread.ts new file mode 100644 index 00000000..cad40930 --- /dev/null +++ b/ui/packages/platform/src/api/bot/convertThread.ts @@ -0,0 +1,29 @@ +import {request} from "../../helpers/request"; + +export const convertThread = async (thread_id: string): Promise<{ response: { final_thread_id: string, msg: string } | null; error: Response | null }> => { + const apiServer = process.env.REACT_APP_BOT_API_URL || ''; + + try { + const response = await request( + `/convert_thread`, + { + method: 'POST', + body: JSON.stringify({ + thread_id + }) + }, + apiServer + ); + + if (!response.ok) { + return { response: null, error: response }; + } + + const responseData = await response.json(); + + return { response: responseData, error: null }; + + } catch (error) { + return { response: null, error: error as Response }; + } +} \ No newline at end of file diff --git a/ui/packages/platform/src/components/Dashboard/Dashboard.tsx b/ui/packages/platform/src/components/Dashboard/Dashboard.tsx index b10a35c7..641d51a6 100644 --- a/ui/packages/platform/src/components/Dashboard/Dashboard.tsx +++ b/ui/packages/platform/src/components/Dashboard/Dashboard.tsx @@ -46,6 +46,7 @@ import { ProductCardWrapper } from 'components/ProductCard/ProductCardWrapper' import { DashboardProps } from 'components/Dashboard/DashboardWrapper' import { FilteredTableMessage } from 'components/AccessTokens/FilteredTableMessage/FilteredTableMessage' import { CreatedDbLabCards } from 'components/CreateDbLabCards/CreateDbLabCards' +import { convertThread } from "../../api/bot/convertThread"; interface DashboardWithStylesProps extends DashboardProps { classes: ClassesType @@ -163,6 +164,8 @@ class Dashboard extends Component { }) Actions.refresh() + + this.convertThreadAndRedirectToBot() } componentWillUnmount() { @@ -212,6 +215,24 @@ class Dashboard extends Component { this.setState({ filterValue: event.target.value }) } + convertThreadAndRedirectToBot = async () => { + const cookieName = "pgai_tmp_thread_id="; + const cookies = document.cookie.split(';').map(cookie => cookie.trim()); + const pgaiTmpThreadId = cookies.find(cookie => cookie.startsWith(cookieName))?.substring(cookieName.length) || null; + + if (pgaiTmpThreadId) { + try { + const data = await convertThread(pgaiTmpThreadId); + if (data?.response?.final_thread_id) { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname.split('.').slice(-2).join('.')}`; + this.props.history.push(`demo/bot/${data.response.final_thread_id}`); + } + } catch (error) { + console.error('Error converting thread:', error); + } + } + } + render() { const renderProjects = this.props.onlyProjects diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index c114f03a..fac12246 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -6,7 +6,7 @@ */ import { Component } from 'react' -import { Switch, Route, NavLink, Redirect } from 'react-router-dom' +import { Switch, Route, NavLink, Redirect, useRouteMatch } from 'react-router-dom' import { AppBar, Toolbar, @@ -291,6 +291,7 @@ function ProjectWrapper(parentProps: Omit) { } function OrganizationMenu(parentProps: OrganizationMenuProps) { + const isDemoOrg = useRouteMatch(`/${settings.demoOrgAlias}`) if ( parentProps.env && parentProps.env.data && @@ -352,7 +353,7 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { - {/* AI BotNEW - */} + } Date: Mon, 5 Aug 2024 16:20:04 +0000 Subject: [PATCH 046/111] Remove Intercom widget --- ui/packages/platform/public/index.html | 7 -- .../src/components/IndexPage/IndexPage.tsx | 7 -- .../src/components/OrgForm/OrgForm.tsx | 14 ++- .../src/components/SharedUrl/SharedUrl.tsx | 9 -- .../platform/src/hooks/useFloatingIntercom.ts | 96 ------------------- .../platform/src/hooks/useHideIntercom.ts | 38 -------- .../platform/src/pages/Bot/BotWrapper.tsx | 2 - .../src/pages/Bot/Command/Command.tsx | 2 - .../src/pages/JoeInstance/Command/index.tsx | 7 -- ui/packages/platform/src/react-app-env.d.ts | 5 - ui/packages/platform/src/stores/store.js | 36 ------- 11 files changed, 6 insertions(+), 217 deletions(-) delete mode 100644 ui/packages/platform/src/hooks/useFloatingIntercom.ts delete mode 100644 ui/packages/platform/src/hooks/useHideIntercom.ts diff --git a/ui/packages/platform/public/index.html b/ui/packages/platform/public/index.html index d7c74485..87b69b9c 100644 --- a/ui/packages/platform/public/index.html +++ b/ui/packages/platform/public/index.html @@ -45,12 +45,5 @@ You need to enable JavaScript to run this app.
- - diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index fac12246..5676751a 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -1322,13 +1322,6 @@ class IndexPage extends Component { if (raw) { return ( <> - { No   To confirm  - - window.Intercom && window.Intercom('show') - } + contact support - + )} diff --git a/ui/packages/platform/src/components/SharedUrl/SharedUrl.tsx b/ui/packages/platform/src/components/SharedUrl/SharedUrl.tsx index 9f45502c..4352090b 100644 --- a/ui/packages/platform/src/components/SharedUrl/SharedUrl.tsx +++ b/ui/packages/platform/src/components/SharedUrl/SharedUrl.tsx @@ -168,15 +168,6 @@ class SharedUrl extends Component { return ( <> - {page} {showBanner && banner} diff --git a/ui/packages/platform/src/hooks/useFloatingIntercom.ts b/ui/packages/platform/src/hooks/useFloatingIntercom.ts deleted file mode 100644 index 1335245c..00000000 --- a/ui/packages/platform/src/hooks/useFloatingIntercom.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { MutableRefObject, useRef } from 'react' -import { useInterval } from 'use-interval' -import { makeStyles } from '@material-ui/core' - -import { createTransitionInteractive } from '@postgres.ai/shared/styles/vars' - -const useStyles = makeStyles( - { - intercom: { - transition: createTransitionInteractive('transform'), - }, - }, - { index: 1 }, -) - -const UPDATE_INTERVAL = 1000 - -// Intercom has two elements as launcher buttons. -const INTERCOM_BUTTON_SELECTOR = `div[aria-label="Open Intercom Messenger"]` -const INTERCOM_IFRAME_SELECTOR = `iframe[name="intercom-launcher-frame"]` - -// Min indent between target and intercom launcher. -const MIN_INTERCOM_INDENT_PX = 8 - -// Check is intercom launcher intersected with target including "MIN_INTERCOM_INDENT_PX". -// Intercom position can be corrected by current shift "intercomShiftY". -const checkIsIntersectedY = ( - targetRect: DOMRect, - intercomRect: DOMRect, - intercomShiftY: number | undefined = 0, -) => { - const targetTop = targetRect.top - MIN_INTERCOM_INDENT_PX - const targetBottom = targetRect.bottom + MIN_INTERCOM_INDENT_PX - - const intercomTop = intercomRect.top + intercomShiftY - const intercomBottom = intercomRect.bottom + intercomShiftY - - return !(targetBottom < intercomTop || intercomBottom < targetTop) -} - -// Needed shift delta between target and intercom launcher based on the current position. -const getIntercomShiftYDelta = (targetRect: DOMRect, intercomRect: DOMRect) => - intercomRect.bottom - targetRect.top + MIN_INTERCOM_INDENT_PX - -export const useFloatingIntercom = ( - targetRef: MutableRefObject, -) => { - const classes = useStyles() - - const currentShiftYRef = useRef(0) - - // Recalculate intercom position every UPDATE_INTERVAL. - useInterval(() => { - if (!targetRef.current) return - const targetRect = targetRef.current.getBoundingClientRect() - - // TODO?(Anton): Possible optimization search later. - // Find intercom launcher element. - const intercomElement = (window.document.querySelector( - INTERCOM_BUTTON_SELECTOR, - ) ?? window.document.querySelector(INTERCOM_IFRAME_SELECTOR)) as - | HTMLDivElement - | HTMLIFrameElement - - if (!intercomElement) return - - // Update intercom shift and keep it. - const setShiftY = (shiftY: number) => { - intercomElement.style.transform = `translateY(-${shiftY}px)` - currentShiftYRef.current = shiftY - } - - // Make intercom moving smooth. - intercomElement.classList.add(classes.intercom) - - const intercomRect = intercomElement.getBoundingClientRect() - - // Can intercom be intersected with target in the initial position. - const isIntersectedInitially = checkIsIntersectedY( - targetRect, - intercomRect, - currentShiftYRef.current, - ) - - // Intercom in the initial position will not be intersected, reset his shift. - if (!isIntersectedInitially) { - setShiftY(0) - return - } - - // In other case shift intercom to the top relative to the target. - const shiftYDelta = getIntercomShiftYDelta(targetRect, intercomRect) - const shiftY = currentShiftYRef.current + shiftYDelta - setShiftY(shiftY) - }, UPDATE_INTERVAL) -} diff --git a/ui/packages/platform/src/hooks/useHideIntercom.ts b/ui/packages/platform/src/hooks/useHideIntercom.ts deleted file mode 100644 index 8667d310..00000000 --- a/ui/packages/platform/src/hooks/useHideIntercom.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useEffect } from 'react'; - -const INTERCOM_BUTTON_SELECTOR = `div[aria-label="Open Intercom Messenger"]`; -const INTERCOM_IFRAME_SELECTOR = `iframe[name="intercom-launcher-frame"]`; - -export const useHideIntercom = () => { - useEffect(() => { - const intercomButton = document.querySelector(INTERCOM_BUTTON_SELECTOR); - const intercomIframe = document.querySelector(INTERCOM_IFRAME_SELECTOR); - - const originalButtonDisplay = intercomButton ? (intercomButton as HTMLElement).style.display : ''; - const originalIframeDisplay = intercomIframe ? (intercomIframe as HTMLElement).style.display : ''; - - const hideIntercom = () => { - if (intercomButton) { - (intercomButton as HTMLElement).style.display = 'none'; - } - if (intercomIframe) { - (intercomIframe as HTMLElement).style.display = 'none'; - } - }; - - const showIntercom = () => { - if (intercomButton) { - (intercomButton as HTMLElement).style.display = originalButtonDisplay || 'block'; - } - if (intercomIframe) { - (intercomIframe as HTMLElement).style.display = originalIframeDisplay || 'inline'; - } - }; - - hideIntercom(); - - return () => { - showIntercom(); - }; - }, []); -}; \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/BotWrapper.tsx b/ui/packages/platform/src/pages/Bot/BotWrapper.tsx index 6f254e4c..d8676010 100644 --- a/ui/packages/platform/src/pages/Bot/BotWrapper.tsx +++ b/ui/packages/platform/src/pages/Bot/BotWrapper.tsx @@ -2,7 +2,6 @@ import { BotPage } from "./index"; import {RouteComponentProps} from "react-router"; import {AlertSnackbarProvider} from "@postgres.ai/shared/components/AlertSnackbar/useAlertSnackbar"; import { AiBotProvider } from "./hooks"; -import { useHideIntercom } from "../../hooks/useHideIntercom"; export interface BotWrapperProps { envData: { @@ -25,7 +24,6 @@ export interface BotWrapperProps { export const BotWrapper = (props: BotWrapperProps) => { - useHideIntercom(); return ( diff --git a/ui/packages/platform/src/pages/Bot/Command/Command.tsx b/ui/packages/platform/src/pages/Bot/Command/Command.tsx index 7c100aca..53633af8 100644 --- a/ui/packages/platform/src/pages/Bot/Command/Command.tsx +++ b/ui/packages/platform/src/pages/Bot/Command/Command.tsx @@ -16,8 +16,6 @@ import { theme } from "@postgres.ai/shared/styles/theme"; import { isMobileDevice } from "../../../utils/utils"; import { useAiBot } from "../hooks"; import { ReadyState } from "react-use-websocket"; -import { useFloatingIntercom } from "../../../hooks/useFloatingIntercom"; - type Props = { threadId?: string diff --git a/ui/packages/platform/src/pages/JoeInstance/Command/index.tsx b/ui/packages/platform/src/pages/JoeInstance/Command/index.tsx index 488f5ae6..55b0b649 100644 --- a/ui/packages/platform/src/pages/JoeInstance/Command/index.tsx +++ b/ui/packages/platform/src/pages/JoeInstance/Command/index.tsx @@ -4,7 +4,6 @@ import { makeStyles } from '@material-ui/core' import { Button } from '@postgres.ai/shared/components/Button' import { TextField } from '@postgres.ai/shared/components/TextField' -import { useFloatingIntercom } from 'hooks/useFloatingIntercom' import { checkIsSendCmd, @@ -133,11 +132,6 @@ export const Command = React.memo((props: Props) => { inputRef.current.focus() }, []) - // Floating intercom. - const sendButtonRef = useRef(null) - - useFloatingIntercom(sendButtonRef) - return (
{ diff --git a/ui/packages/platform/src/react-app-env.d.ts b/ui/packages/platform/src/react-app-env.d.ts index b9a38a97..02edd50c 100644 --- a/ui/packages/platform/src/react-app-env.d.ts +++ b/ui/packages/platform/src/react-app-env.d.ts @@ -16,8 +16,3 @@ declare namespace NodeJS { readonly BUILD_TIMESTAMP: number } } - -// Intercom types. -declare interface Window { - Intercom?: (action: 'show') => void -} diff --git a/ui/packages/platform/src/stores/store.js b/ui/packages/platform/src/stores/store.js index 52be1370..8785cda6 100644 --- a/ui/packages/platform/src/stores/store.js +++ b/ui/packages/platform/src/stores/store.js @@ -380,42 +380,6 @@ const Store = Reflux.createStore({ if (!this.data.userProfile.error && data.length > 0) { this.data.userProfile.data = data[0]; this.data.userProfile.isProcessed = true; - - if (window.intercomSettings) { - let name = null; - - if (data[0].info.email.split('@').length > 0) { - name = data[0].info.email.split('@')[0]; - } - - if (data[0].info.first_name) { - name = data[0].info.first_name; - if (data[0].info.last_name) { - name = name + ' ' + data[0].info.last_name; - } - } - window.intercomSettings.name = name; - window.intercomSettings.email = data[0].info.email; - window.intercomSettings.user_id = data[0].info.id; - window.intercomSettings.created_at = data[0].info.created_at; - - if (data[0].orgs) { - window.intercomSettings.companies = []; - - for (let i in data[0].orgs) { - if (data[0].orgs.hasOwnProperty(i)) { - window.intercomSettings.companies.push({ - company_id: data[0].orgs[i].id, - name: data[0].orgs[i].name - }); - } - } - } - - if (window.Intercom) { - window.Intercom('update'); - } - } } this.trigger(this.data); From 2a460f7b1cada4c1fd321bba15a273511de8c6e8 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Mon, 5 Aug 2024 16:34:57 +0000 Subject: [PATCH 047/111] Bot UI: Check user orgs before redirect to /demo/bot after registration --- .../src/components/Dashboard/Dashboard.tsx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/ui/packages/platform/src/components/Dashboard/Dashboard.tsx b/ui/packages/platform/src/components/Dashboard/Dashboard.tsx index 641d51a6..8f0c3d5b 100644 --- a/ui/packages/platform/src/components/Dashboard/Dashboard.tsx +++ b/ui/packages/platform/src/components/Dashboard/Dashboard.tsx @@ -107,13 +107,33 @@ class Dashboard extends Component { const orgId = this.props.orgId const onlyProjects = this.props.onlyProjects - this.unsubscribe = (Store.listen as RefluxTypes['listen'])(function () { + this.unsubscribe = (Store.listen as RefluxTypes['listen'])(async function () { that.setState({ data: this.data }) + const auth: DashboardState['data']['auth'] = this.data && this.data.auth ? this.data.auth : null const userProfile: DashboardState['data']['userProfile'] = this.data && this.data.userProfile ? this.data.userProfile : null + const cookieName = "pgai_tmp_thread_id="; + const cookies = document.cookie.split(';').map(cookie => cookie.trim()); + const pgaiTmpThreadId = cookies.find(cookie => cookie.startsWith(cookieName))?.substring(cookieName.length) || null; + if (pgaiTmpThreadId) { + try { + const data = await convertThread(pgaiTmpThreadId); + if (data?.response?.final_thread_id) { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname.split('.').slice(-2).join('.')}`; + if (userProfile && userProfile.data && userProfile.data.orgs) { + if (userProfile.data.orgs.hasOwnProperty('demo')) { + that.props.history.push(`demo/bot/${data.response.final_thread_id}`); + } + } + } + } catch (error) { + console.error('Error converting thread:', error); + } + } + if (onlyProjects) { const projects: DashboardState['data']['projects'] = this.data && this.data.projects ? this.data.projects : null @@ -164,8 +184,6 @@ class Dashboard extends Component { }) Actions.refresh() - - this.convertThreadAndRedirectToBot() } componentWillUnmount() { @@ -215,24 +233,6 @@ class Dashboard extends Component { this.setState({ filterValue: event.target.value }) } - convertThreadAndRedirectToBot = async () => { - const cookieName = "pgai_tmp_thread_id="; - const cookies = document.cookie.split(';').map(cookie => cookie.trim()); - const pgaiTmpThreadId = cookies.find(cookie => cookie.startsWith(cookieName))?.substring(cookieName.length) || null; - - if (pgaiTmpThreadId) { - try { - const data = await convertThread(pgaiTmpThreadId); - if (data?.response?.final_thread_id) { - document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname.split('.').slice(-2).join('.')}`; - this.props.history.push(`demo/bot/${data.response.final_thread_id}`); - } - } catch (error) { - console.error('Error converting thread:', error); - } - } - } - render() { const renderProjects = this.props.onlyProjects From 3a456108f82dd5b775cb305c30eef6dd93104dc7 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 15 Aug 2024 01:18:34 +0000 Subject: [PATCH 048/111] Refactor message handling to real-time WebSocket updates --- .../src/pages/Bot/Command/Command.tsx | 5 +- .../pages/Bot/Messages/Message/Message.tsx | 16 ++- .../src/pages/Bot/Messages/Messages.tsx | 23 ++-- ui/packages/platform/src/pages/Bot/hooks.tsx | 114 ++++++++++++------ ui/packages/platform/src/pages/Bot/index.tsx | 4 +- .../platform/src/types/api/entities/bot.ts | 7 ++ ui/packages/platform/src/utils/format.ts | 2 + 7 files changed, 120 insertions(+), 51 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/Command/Command.tsx b/ui/packages/platform/src/pages/Bot/Command/Command.tsx index 53633af8..0ce8a89a 100644 --- a/ui/packages/platform/src/pages/Bot/Command/Command.tsx +++ b/ui/packages/platform/src/pages/Bot/Command/Command.tsx @@ -85,10 +85,11 @@ export const Command = React.memo((props: Props) => { wsLoading, loading, sendMessage, - chatVisibility + chatVisibility, + isStreamingInProcess } = useAiBot(); - const sendDisabled = error !== null || loading || wsLoading || wsReadyState !== ReadyState.OPEN; + const sendDisabled = error !== null || loading || wsLoading || wsReadyState !== ReadyState.OPEN || isStreamingInProcess; // Handle value. const [value, setValue] = useState('') diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx index d8922b0c..b57d2fcb 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx @@ -203,6 +203,7 @@ const useStyles = makeStyles( marginBlockEnd: '1em', marginInlineStart: 0, marginInlineEnd: 0, + //animation: `$typing 0.5s steps(30, end), $blinkCaret 0.75s step-end infinite`, }, '& .MuiExpansionPanel-root div': { marginBlockStart: 0, @@ -216,6 +217,7 @@ const useStyles = makeStyles( marginInlineStart: 0, marginInlineEnd: 0, fontSize: 14, + color: colors.pgaiDarkGray, '&:after': { overflow: 'hidden', display: 'inline-block', @@ -229,7 +231,16 @@ const useStyles = makeStyles( 'to': { width: '0.9em' }, - } + }, + '@keyframes typing': { + from: { width: 0 }, + to: { width: '100%' }, + }, + '@keyframes blinkCaret': { + from: { borderRightColor: 'transparent' }, + to: { borderRightColor: 'transparent' }, + '50%': { borderRightColor: 'black' }, + }, }), ) @@ -336,7 +347,8 @@ export const Message = React.memo((props: MessageProps) => {
{isLoading - ?
+ ? +
{stateMessage && stateMessage.state ? stateMessage.state : 'Thinking'}
diff --git a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx index 3ba1e8b9..d2f1b5ce 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx @@ -111,15 +111,8 @@ const useStyles = makeStyles( padding: 10 } }), - ) -type MessagesProps = { - messages: BotMessageWithDebugInfo[] | null - isLoading: boolean - isWaitingForAnswer: boolean -} - type Time = string type FormattedTime = { @@ -127,7 +120,13 @@ type FormattedTime = { } export const Messages = React.memo(() => { - const { messages, loading: isLoading, wsLoading: isWaitingForAnswer, stateMessage } = useAiBot(); + const { + messages, + loading: isLoading, + wsLoading: isWaitingForAnswer, + stateMessage, + currentStreamMessage + } = useAiBot(); const rootRef = useRef(null); const wrapperRef = useRef(null); @@ -275,6 +274,14 @@ export const Messages = React.memo(() => { /> ) })} + { + currentStreamMessage && + } {isWaitingForAnswer && } diff --git a/ui/packages/platform/src/pages/Bot/hooks.tsx b/ui/packages/platform/src/pages/Bot/hooks.tsx index ea189f85..e37961d3 100644 --- a/ui/packages/platform/src/pages/Bot/hooks.tsx +++ b/ui/packages/platform/src/pages/Bot/hooks.tsx @@ -8,7 +8,7 @@ import React, { createContext, useCallback, useContext, useEffect, useState } from "react"; import useWebSocket, {ReadyState} from "react-use-websocket"; import { useLocation } from "react-router-dom"; -import { BotMessage, DebugMessage, AiModel, StateMessage } from "../../types/api/entities/bot"; +import { BotMessage, DebugMessage, AiModel, StateMessage, StreamMessage } from "../../types/api/entities/bot"; import {getChatsWithWholeThreads} from "../../api/bot/getChatsWithWholeThreads"; import {getChats} from "api/bot/getChats"; import {useAlertSnackbar} from "@postgres.ai/shared/components/AlertSnackbar/useAlertSnackbar"; @@ -58,6 +58,8 @@ type UseAiBotReturnType = { aiModelsLoading: UseAiModelsList['loading']; debugMessagesLoading: boolean; stateMessage: StateMessage | null; + isStreamingInProcess: boolean + currentStreamMessage: StreamMessage | null } type UseAiBotArgs = { @@ -90,6 +92,8 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => const [wsLoading, setWsLoading] = useState(false); const [chatVisibility, setChatVisibility] = useState('public'); const [stateMessage, setStateMessage] = useState(null) + const [currentStreamMessage, setCurrentStreamMessage] = useState(null) + const [isStreamingInProcess, setStreamingInProcess] = useState(false) const [isChangeVisibilityLoading, setIsChangeVisibilityLoading] = useState(false); @@ -102,51 +106,35 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => const onWebSocketMessage = (event: WebSocketEventMap['message']) => { if (event.data) { - const messageData: BotMessage | DebugMessage | StateMessage = JSON.parse(event.data); + const messageData: BotMessage | DebugMessage | StateMessage | StreamMessage = JSON.parse(event.data); if (messageData) { const isThreadMatching = threadId && threadId === messageData.thread_id; const isParentMatching = !threadId && 'parent_id' in messageData && messageData.parent_id && messages; const isDebugMessage = messageData.type === 'debug'; const isStateMessage = messageData.type === 'state'; - if (isThreadMatching || isParentMatching || isDebugMessage || isStateMessage) { - if (isDebugMessage) { - let currentDebugMessages = [...(debugMessages || [])]; - currentDebugMessages.push(messageData) - setDebugMessages(currentDebugMessages) - } else if (isStateMessage) { - if (isThreadMatching || !threadId) { - if (messageData.state) { - setStateMessage(messageData) - } else { - setStateMessage(null) - } - } - } else { - // Check if the last message needs its data updated - let currentMessages = [...(messages || [])]; - const lastMessage = currentMessages[currentMessages.length - 1]; - if (lastMessage && !lastMessage.id && messageData.parent_id) { - lastMessage.id = messageData.parent_id; - lastMessage.created_at = messageData.created_at; - lastMessage.is_public = messageData.is_public; - } - - currentMessages.push(messageData); - setMessages(currentMessages); - setWsLoading(false); - if (document.visibilityState === "hidden") { - if (Notification.permission === "granted") { - new Notification("New message", { - body: 'New message from Postgres.AI Bot', - icon: '/images/bot_avatar.png' - }); - } - } + const isStreamMessage = messageData.type === 'stream'; + + if (isThreadMatching || isParentMatching || isDebugMessage || isStateMessage || isStreamMessage) { + switch (messageData.type) { + case 'debug': + handleDebugMessage(messageData) + break; + case 'state': + handleStateMessage(messageData, Boolean(isThreadMatching)) + break; + case 'stream': + handleStreamMessage(messageData, Boolean(isThreadMatching)) + break; + case 'message': + handleBotMessage(messageData) + break; } } else if (threadId !== messageData.thread_id) { const threadInList = chatsList?.find((item) => item.thread_id === messageData.thread_id) if (!threadInList) getChatsList() - setWsLoading(false); + if (currentStreamMessage) setCurrentStreamMessage(null) + if (wsLoading) setWsLoading(false); + if (isStreamingInProcess) setStreamingInProcess(false) } } else { showMessage('An error occurred. Please try again') @@ -158,6 +146,56 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => setLoading(false); } + const handleDebugMessage = (message: DebugMessage) => { + let currentDebugMessages = [...(debugMessages || [])]; + currentDebugMessages.push(message) + setDebugMessages(currentDebugMessages) + } + + const handleStateMessage = (message: StateMessage, isThreadMatching?: boolean) => { + if (isThreadMatching || !threadId) { + if (message.state) { + setStateMessage(message) + } else { + setStateMessage(null) + } + } + } + + const handleStreamMessage = (message: StreamMessage, isThreadMatching?: boolean) => { + if (isThreadMatching || !threadId) { + if (!isStreamingInProcess) setStreamingInProcess(true) + setCurrentStreamMessage(message) + setWsLoading(false); + } + } + + const handleBotMessage = (message: BotMessage) => { + if (messages && messages.length > 0) { + let currentMessages = [...messages]; + const lastMessage = currentMessages[currentMessages.length - 1]; + if (lastMessage && !lastMessage.id && message.parent_id) { + lastMessage.id = message.parent_id; + lastMessage.created_at = message.created_at; + lastMessage.is_public = message.is_public; + } + + currentMessages.push(message); + if (currentStreamMessage) setCurrentStreamMessage(null) + setMessages(currentMessages); + setWsLoading(false); + setStreamingInProcess(false); + if (document.visibilityState === "hidden") { + if (Notification.permission === "granted") { + new Notification("New message", { + body: 'New message from Postgres.AI Bot', + icon: '/images/bot_avatar.png' + }); + } + } + } + } + const onWebSocketOpen = () => { console.log('WebSocket connection established'); if (threadId) { @@ -381,6 +419,8 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => debugMessages, debugMessagesLoading, stateMessage, + isStreamingInProcess, + currentStreamMessage } } diff --git a/ui/packages/platform/src/pages/Bot/index.tsx b/ui/packages/platform/src/pages/Bot/index.tsx index 06693bbe..aa135d31 100644 --- a/ui/packages/platform/src/pages/Bot/index.tsx +++ b/ui/packages/platform/src/pages/Bot/index.tsx @@ -156,9 +156,9 @@ export const BotPage = (props: BotPageProps) => { } useEffect(() => { - if (!match.params.threadId && !prevThreadId && messages && messages.length > 1 && messages[1].parent_id) { + if (!match.params.threadId && !prevThreadId && messages && messages.length > 0 && messages[0].id) { // hack that skip additional loading chats_ancestors_and_descendants - history.replace(`/${match.params.org}/bot/${messages[1].parent_id}`, { skipReloading: true }) + history.replace(`/${match.params.org}/bot/${messages[0].id}`, { skipReloading: true }) getChatsList(); } else if (prevThreadId && !match.params.threadId) { clearChat() diff --git a/ui/packages/platform/src/types/api/entities/bot.ts b/ui/packages/platform/src/types/api/entities/bot.ts index 334b24df..0447ce58 100644 --- a/ui/packages/platform/src/types/api/entities/bot.ts +++ b/ui/packages/platform/src/types/api/entities/bot.ts @@ -40,4 +40,11 @@ export type StateMessage = { type: 'state' state: string | null thread_id: string +} + +export type StreamMessage = { + type: 'stream' + content: string + ai_model: string + thread_id: string } \ No newline at end of file diff --git a/ui/packages/platform/src/utils/format.ts b/ui/packages/platform/src/utils/format.ts index 606fd3c9..2e1aa504 100644 --- a/ui/packages/platform/src/utils/format.ts +++ b/ui/packages/platform/src/utils/format.ts @@ -264,6 +264,8 @@ const Format = { }, timeAgo: function (date: string | Date): string | null { + if (!date) return null + const now = new Date(); const past = new Date(date); const diff = Math.abs(now.getTime() - past.getTime()); From 07a2d1bd657ba7a9fbc33b83a79e877a2dd3f30d Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 15 Aug 2024 14:22:03 +0000 Subject: [PATCH 049/111] Bot UI: Fixes multiple thread creation --- .../platform/src/api/bot/convertThread.ts | 45 +++++++++---------- .../src/components/Dashboard/Dashboard.tsx | 26 ++++++----- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/ui/packages/platform/src/api/bot/convertThread.ts b/ui/packages/platform/src/api/bot/convertThread.ts index cad40930..bd9d9b2a 100644 --- a/ui/packages/platform/src/api/bot/convertThread.ts +++ b/ui/packages/platform/src/api/bot/convertThread.ts @@ -1,29 +1,24 @@ import {request} from "../../helpers/request"; -export const convertThread = async (thread_id: string): Promise<{ response: { final_thread_id: string, msg: string } | null; error: Response | null }> => { +export const convertThread = (thread_id: string): Promise<{ response: { final_thread_id: string, msg: string } | null; error: Response | null }> => { const apiServer = process.env.REACT_APP_BOT_API_URL || ''; - try { - const response = await request( - `/convert_thread`, - { - method: 'POST', - body: JSON.stringify({ - thread_id - }) - }, - apiServer - ); - - if (!response.ok) { - return { response: null, error: response }; - } - - const responseData = await response.json(); - - return { response: responseData, error: null }; - - } catch (error) { - return { response: null, error: error as Response }; - } -} \ No newline at end of file + return request( + `/convert_thread`, + { + method: 'POST', + body: JSON.stringify({ thread_id }), + }, + apiServer + ) + .then(async (response) => { + if (!response.ok) { + return { response: null, error: response }; + } + const responseData = await response.json(); + return { response: responseData, error: null }; + }) + .catch((error: Response) => { + return { response: null, error }; + }); +}; \ No newline at end of file diff --git a/ui/packages/platform/src/components/Dashboard/Dashboard.tsx b/ui/packages/platform/src/components/Dashboard/Dashboard.tsx index 8f0c3d5b..6764a30a 100644 --- a/ui/packages/platform/src/components/Dashboard/Dashboard.tsx +++ b/ui/packages/platform/src/components/Dashboard/Dashboard.tsx @@ -101,13 +101,14 @@ interface DashboardState { } class Dashboard extends Component { + isThreadConverted = false; unsubscribe: Function componentDidMount() { const that = this const orgId = this.props.orgId const onlyProjects = this.props.onlyProjects - this.unsubscribe = (Store.listen as RefluxTypes['listen'])(async function () { + this.unsubscribe = (Store.listen as RefluxTypes['listen'])(function () { that.setState({ data: this.data }) const auth: DashboardState['data']['auth'] = @@ -118,20 +119,25 @@ class Dashboard extends Component { const cookieName = "pgai_tmp_thread_id="; const cookies = document.cookie.split(';').map(cookie => cookie.trim()); const pgaiTmpThreadId = cookies.find(cookie => cookie.startsWith(cookieName))?.substring(cookieName.length) || null; - if (pgaiTmpThreadId) { + if (pgaiTmpThreadId && !that.isThreadConverted) { + that.isThreadConverted = true; try { - const data = await convertThread(pgaiTmpThreadId); - if (data?.response?.final_thread_id) { - document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname.split('.').slice(-2).join('.')}`; - if (userProfile && userProfile.data && userProfile.data.orgs) { - if (userProfile.data.orgs.hasOwnProperty('demo')) { - that.props.history.push(`demo/bot/${data.response.final_thread_id}`); + convertThread(pgaiTmpThreadId) + .then(({response, error}) => { + if (response?.final_thread_id) { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname.split('.').slice(-2).join('.')}`; + if (userProfile && userProfile.data && userProfile.data.orgs) { + if (userProfile.data.orgs.hasOwnProperty('demo')) { + that.props.history.push(`demo/bot/${response.final_thread_id}`); + } + } } - } - } + }) } catch (error) { console.error('Error converting thread:', error); } + } else { + document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname.split('.').slice(-2).join('.')}`; } if (onlyProjects) { From 90971ed7a40a6c274f3e27ee59619353ea609e30 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Tue, 20 Aug 2024 01:21:15 +0000 Subject: [PATCH 050/111] Bot UI: Prevents clearing the input field after streaming when creating a chat --- .../platform/src/pages/Bot/Command/Command.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/Command/Command.tsx b/ui/packages/platform/src/pages/Bot/Command/Command.tsx index 0ce8a89a..35f54f77 100644 --- a/ui/packages/platform/src/pages/Bot/Command/Command.tsx +++ b/ui/packages/platform/src/pages/Bot/Command/Command.tsx @@ -1,8 +1,10 @@ -import React, { useState, useRef, useEffect, useMemo } from 'react' +import React, { useState, useRef, useEffect } from 'react' import { makeStyles } from '@material-ui/core' import SendRoundedIcon from '@material-ui/icons/SendRounded'; import IconButton from "@material-ui/core/IconButton"; import { TextField } from '@postgres.ai/shared/components/TextField' +import { ReadyState } from "react-use-websocket"; +import { useLocation } from "react-router-dom"; import { checkIsSendCmd, checkIsNewLineCmd, @@ -15,7 +17,6 @@ import { useCaret } from './useCaret' import { theme } from "@postgres.ai/shared/styles/theme"; import { isMobileDevice } from "../../../utils/utils"; import { useAiBot } from "../hooks"; -import { ReadyState } from "react-use-websocket"; type Props = { threadId?: string @@ -103,6 +104,8 @@ export const Command = React.memo((props: Props) => { // Input caret. const caret = useCaret(inputRef) + let location = useLocation<{skipReloading?: boolean}>(); + const onSend = async (message: string) => { await sendMessage({ content: message, @@ -186,11 +189,11 @@ export const Command = React.memo((props: Props) => { // Skip other keyboard events to fill input. } - // Autofocus. + // Autofocus and clear on thread changed useEffect(() => { if (!inputRef.current) return if (window.innerWidth > theme.breakpoints.values.md) inputRef.current.focus() - setValue('') + if (!location.state?.skipReloading) setValue('') }, [threadId]); return ( From e74f840302d6bac25d57a9be75d4752410ad5576 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Tue, 20 Aug 2024 01:21:32 +0000 Subject: [PATCH 051/111] Bot UI: Debug messages links fix --- ui/packages/platform/src/pages/Bot/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/utils.ts b/ui/packages/platform/src/pages/Bot/utils.ts index d6b45f20..d6122ad6 100644 --- a/ui/packages/platform/src/pages/Bot/utils.ts +++ b/ui/packages/platform/src/pages/Bot/utils.ts @@ -37,10 +37,10 @@ export const createMessageFragment = (messages: DebugMessage[]): DocumentFragmen messages.forEach((item) => { const textBeforeLink = `[${item.created_at}]: `; - const parts = item.content.split(/(https?:\/\/[^\s]+)/g); + const parts = item.content.split(/(https?:\/\/[^\s)"']+)/g); const messageContent = parts.map((part) => { - if (/https?:\/\/[^\s]+/.test(part)) { + if(/https?:\/\/[^\s)"']+/.test(part)) { const link = document.createElement('a'); link.href = part; link.target = '_blank'; From 0c20d3b79b27d6a969ad668107897cf89879c533 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Tue, 20 Aug 2024 01:21:44 +0000 Subject: [PATCH 052/111] Bot UI: Refactor timeAgo function to use existing pluralize utility --- ui/packages/platform/src/utils/format.ts | 10 ++++++---- ui/packages/platform/src/utils/time.ts | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ui/packages/platform/src/utils/format.ts b/ui/packages/platform/src/utils/format.ts index 2e1aa504..c49de470 100644 --- a/ui/packages/platform/src/utils/format.ts +++ b/ui/packages/platform/src/utils/format.ts @@ -6,6 +6,7 @@ */ import moment from 'moment' +import { pluralize } from "./time"; const Format = { formatSeconds: function (seconds: number, decimal: number, separator = ' ') { @@ -266,6 +267,7 @@ const Format = { timeAgo: function (date: string | Date): string | null { if (!date) return null + const now = new Date(); const past = new Date(date); const diff = Math.abs(now.getTime() - past.getTime()); @@ -275,13 +277,13 @@ const Format = { const days = Math.floor(hours / 24); if (seconds < 60) { - return `${seconds} seconds ago`; + return `${seconds} ${pluralize('second', 'seconds')(seconds)} ago`; } else if (minutes < 60) { - return `${minutes} minutes ago`; + return `${minutes} ${pluralize('minute', 'minutes')(minutes)} ago`; } else if (hours < 24) { - return `${hours} hours ago`; + return `${hours} ${pluralize('hour', 'hours')(hours)} ago`; } else { - return `${days} days ago`; + return `${days} ${pluralize('day', 'days')(seconds)} ago`; } } } diff --git a/ui/packages/platform/src/utils/time.ts b/ui/packages/platform/src/utils/time.ts index d46057e8..5417b995 100644 --- a/ui/packages/platform/src/utils/time.ts +++ b/ui/packages/platform/src/utils/time.ts @@ -1,4 +1,4 @@ -const pluralize = (single: string, multiple: string) => (val: number) => +export const pluralize = (single: string, multiple: string) => (val: number) => val === 1 ? single : multiple const MS_IN_SECOND = 1000 From 70c987776cb970d0d8954b2b7d02e56856a5e4af Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Tue, 20 Aug 2024 01:22:00 +0000 Subject: [PATCH 053/111] Bot UI: Close chat list by clicking on a list item on mobile devices --- ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx index 0c43c353..9c8fc418 100644 --- a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx +++ b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx @@ -125,6 +125,9 @@ export const ChatsList = (props: ChatsListProps) => { const handleClick = (threadId: string) => { if (onLinkClick) { onLinkClick(threadId) + if (matches) { + onClose() + } } } From bf2ae74bfde70a2da995ab7e3bb38901ff96aef7 Mon Sep 17 00:00:00 2001 From: akartasov Date: Tue, 20 Aug 2024 18:05:43 +0700 Subject: [PATCH 054/111] fix: fileID representation of SQL report files --- .../src/components/ReportFile/ReportFile.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/ui/packages/platform/src/components/ReportFile/ReportFile.tsx b/ui/packages/platform/src/components/ReportFile/ReportFile.tsx index e1106028..6614b0b9 100644 --- a/ui/packages/platform/src/components/ReportFile/ReportFile.tsx +++ b/ui/packages/platform/src/components/ReportFile/ReportFile.tsx @@ -6,7 +6,7 @@ */ import { Component, HTMLAttributeAnchorTarget } from 'react' -import { TextField, Button } from '@material-ui/core' +import { Button, TextField } from '@material-ui/core' import ReactMarkdown from 'react-markdown' import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' @@ -124,15 +124,13 @@ const textAreaStyles = ( class ReportFile extends Component { getFileId() { - let id = parseInt(this.props.match.params.fileId, 10) - /* eslint eqeqeq: 1 */ - id = - id == parseInt(this.props.match.params.fileId) - ? id - : parseInt(this.props.match.params.fileId) - /* eslint eqeqeq: 0 */ - - return id + let fileID = this.props.match.params.fileId + let parseID = parseInt(fileID, 10) + + // To distinct different fileIDs. For example, "72215" and "1_1.sql". + // {ORG_URL}/reports/268/files/72215/md + // {ORG_URL}/reports/268/files/1_1.sql/sql?raw + return (fileID.toString() == parseID.toString()) ? parseID : fileID } componentDidMount() { From 818887cf592802cd43f2f159478b707311c09552 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 30 Aug 2024 15:53:55 +0000 Subject: [PATCH 055/111] Bot UI: Render mermaid diagram --- ui/packages/platform/package.json | 1 + .../Bot/Messages/Message/MermaidDiagram.tsx | 15 + .../pages/Bot/Messages/Message/Message.tsx | 17 +- ui/pnpm-lock.yaml | 487 ++++++++++++++++++ 4 files changed, 515 insertions(+), 5 deletions(-) create mode 100644 ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx diff --git a/ui/packages/platform/package.json b/ui/packages/platform/package.json index 31fb4f5d..fcdb53d2 100644 --- a/ui/packages/platform/package.json +++ b/ui/packages/platform/package.json @@ -51,6 +51,7 @@ "jwt-encode": "^1.0.1", "lodash": "^4.17.15", "md5": "^2.2.1", + "mermaid": "^11.0.2", "mobx": "^6.3.2", "mobx-react-lite": "^3.2.0", "moment": "^2.24.0", diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx new file mode 100644 index 00000000..f6ef24ae --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx @@ -0,0 +1,15 @@ +import React, { useEffect } from 'react'; +import mermaid from 'mermaid'; + +type MermaidDiagramProps = { + chart: string +} + +export const MermaidDiagram = React.memo((props: MermaidDiagramProps) => { + const { chart } = props; + mermaid.initialize({ startOnLoad: true }); + useEffect(() => { + mermaid.contentLoaded(); + }, [chart]); + return
{chart}
; +}) \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx index b57d2fcb..99baab53 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx @@ -10,6 +10,7 @@ import { DebugDialog } from "../../DebugDialog/DebugDialog"; import { CodeBlock } from "./CodeBlock"; import { disallowedHtmlTagsForMarkdown, permalinkLinkBuilder } from "../../utils"; import { StateMessage } from "../../../../types/api/entities/bot"; +import { MermaidDiagram } from "./MermaidDiagram"; type BaseMessageProps = { @@ -275,11 +276,17 @@ export const Message = React.memo((props: MessageProps) => { img: ({ node, ...props }) => , code: ({ node, inline, className, children, ...props }) => { const match = /language-(\w+)/.exec(className || ''); - return !inline ? ( - - ) : ( - {children} - ); + const matchMermaid = /language-mermaid/.test(className || ''); + if (!inline) { + return ( + <> + {matchMermaid && } + + + ) + } else { + return {children} + } }, }), []); diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 70c31a78..5ce19d71 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -362,6 +362,9 @@ importers: md5: specifier: ^2.2.1 version: 2.3.0 + mermaid: + specifier: ^11.0.2 + version: 11.0.2 mobx: specifier: ^6.3.2 version: 6.3.5 @@ -2004,6 +2007,37 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: false + /@braintree/sanitize-url@7.1.0: + resolution: {integrity: sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==} + dev: false + + /@chevrotain/cst-dts-gen@11.0.3: + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + dev: false + + /@chevrotain/gast@11.0.3: + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + dev: false + + /@chevrotain/regexp-to-ast@11.0.3: + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + dev: false + + /@chevrotain/types@11.0.3: + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + dev: false + + /@chevrotain/utils@11.0.3: + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + dev: false + /@colors/colors@1.5.0: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -3102,6 +3136,12 @@ packages: react-is: 17.0.2 dev: false + /@mermaid-js/parser@0.2.0: + resolution: {integrity: sha512-33dyFdhwsX9n4+E8SRj1ulxwAgwCj9RyCMtoqXD5cDfS9F6y9xmvmjFjHoPaViH4H7I7BXD8yP/XEWig5XrHSQ==} + dependencies: + langium: 3.0.0 + dev: false + /@monaco-editor/loader@1.3.2(monaco-editor@0.48.0): resolution: {integrity: sha512-BTDbpHl3e47r3AAtpfVFTlAi7WXv4UQ/xZmz8atKl4q7epQV5e7+JbigFDViWF71VBi4IIBdcWP57Hj+OWuc9g==} peerDependencies: @@ -5383,6 +5423,26 @@ packages: resolution: {integrity: sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==} dev: false + /chevrotain-allstar@0.3.1(chevrotain@11.0.3): + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.17.21 + dev: false + + /chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + dev: false + /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -5704,6 +5764,12 @@ packages: /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + /cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + dependencies: + layout-base: 1.0.2 + dev: false + /cosmiconfig-typescript-loader@1.0.9(@types/node@12.20.33)(cosmiconfig@7.0.1)(typescript@4.8.3): resolution: {integrity: sha512-tRuMRhxN4m1Y8hP9SNYfz7jRwt8lZdWxdjg/ohg5esKmsndJIn4yT96oJVcf5x0eA11taXl+sIp+ielu529k6g==} engines: {node: '>=12', npm: '>=6'} @@ -6198,14 +6264,40 @@ packages: yauzl: 2.10.0 dev: false + /cytoscape-cose-bilkent@4.1.0(cytoscape@3.30.2): + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + dependencies: + cose-base: 1.0.3 + cytoscape: 3.30.2 + dev: false + + /cytoscape@3.30.2: + resolution: {integrity: sha512-oICxQsjW8uSaRmn4UK/jkczKOqTrVqt5/1WL0POiJUT2EKNc9STM4hYFHv917yu55aTBMFNRzymlJhVAiWPCxw==} + engines: {node: '>=0.10'} + dev: false + /d3-array@1.2.4: resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==} dev: false + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + /d3-axis@1.0.12: resolution: {integrity: sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==} dev: false + /d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + dev: false + /d3-brush@1.1.6: resolution: {integrity: sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==} dependencies: @@ -6216,6 +6308,17 @@ packages: d3-transition: 1.3.2 dev: false + /d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: false + /d3-chord@1.0.6: resolution: {integrity: sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==} dependencies: @@ -6223,6 +6326,13 @@ packages: d3-path: 1.0.9 dev: false + /d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + /d3-collection@1.0.7: resolution: {integrity: sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==} dev: false @@ -6238,10 +6348,29 @@ packages: d3-array: 1.2.4 dev: false + /d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + dependencies: + delaunator: 5.0.1 + dev: false + /d3-dispatch@1.0.6: resolution: {integrity: sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==} dev: false + /d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + dev: false + /d3-drag@1.2.5: resolution: {integrity: sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==} dependencies: @@ -6249,6 +6378,14 @@ packages: d3-selection: 1.4.2 dev: false + /d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + dev: false + /d3-dsv@1.2.0: resolution: {integrity: sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==} hasBin: true @@ -6258,16 +6395,38 @@ packages: rw: 1.3.3 dev: false + /d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + dev: false + /d3-ease@1.0.7: resolution: {integrity: sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==} dev: false + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + /d3-fetch@1.2.0: resolution: {integrity: sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==} dependencies: d3-dsv: 1.2.0 dev: false + /d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + dependencies: + d3-dsv: 3.0.1 + dev: false + /d3-flame-graph@2.2.2: resolution: {integrity: sha512-Vo5wqnYA2RsrnBsSYTQB72DhMcduE9dgkf22Fy0f/0qXUpflL3Yo8YKxmzPi6Fioy7/0fgygYYZrf/JbrE1jyQ==} dependencies: @@ -6290,42 +6449,102 @@ packages: d3-timer: 1.0.10 dev: false + /d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + dev: false + /d3-format@1.4.5: resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==} dev: false + /d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: false + /d3-geo@1.12.1: resolution: {integrity: sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==} dependencies: d3-array: 1.2.4 dev: false + /d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + /d3-hierarchy@1.1.9: resolution: {integrity: sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==} dev: false + /d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + dev: false + /d3-interpolate@1.4.0: resolution: {integrity: sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==} dependencies: d3-color: 3.1.0 dev: false + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + /d3-path@1.0.9: resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} dev: false + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: false + /d3-polygon@1.0.6: resolution: {integrity: sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==} dev: false + /d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + dev: false + /d3-quadtree@1.0.7: resolution: {integrity: sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==} dev: false + /d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + dev: false + /d3-random@1.1.2: resolution: {integrity: sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==} dev: false + /d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + dev: false + + /d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + dependencies: + d3-array: 1.2.4 + d3-shape: 1.3.7 + dev: false + /d3-scale-chromatic@1.5.0: resolution: {integrity: sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==} dependencies: @@ -6333,6 +6552,14 @@ packages: d3-interpolate: 1.4.0 dev: false + /d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + dev: false + /d3-scale@2.2.2: resolution: {integrity: sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==} dependencies: @@ -6344,30 +6571,72 @@ packages: d3-time-format: 2.3.0 dev: false + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: false + /d3-selection@1.4.2: resolution: {integrity: sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==} dev: false + /d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + dev: false + /d3-shape@1.3.7: resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} dependencies: d3-path: 1.0.9 dev: false + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + /d3-time-format@2.3.0: resolution: {integrity: sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==} dependencies: d3-time: 1.1.0 dev: false + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: false + /d3-time@1.1.0: resolution: {integrity: sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==} dev: false + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + /d3-timer@1.0.10: resolution: {integrity: sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==} dev: false + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + /d3-transition@1.3.2: resolution: {integrity: sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==} dependencies: @@ -6379,6 +6648,20 @@ packages: d3-timer: 1.0.10 dev: false + /d3-transition@3.0.1(d3-selection@3.0.0): + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + dev: false + /d3-voronoi@1.1.4: resolution: {integrity: sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==} dev: false @@ -6393,6 +6676,17 @@ packages: d3-transition: 1.3.2 dev: false + /d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: false + /d3@5.16.0: resolution: {integrity: sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==} dependencies: @@ -6429,6 +6723,49 @@ packages: d3-zoom: 1.8.3 dev: false + /d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + dev: false + + /dagre-d3-es@7.0.10: + resolution: {integrity: sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==} + dependencies: + d3: 7.9.0 + lodash-es: 4.17.21 + dev: false + /damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: false @@ -6454,6 +6791,10 @@ packages: engines: {node: '>=0.11'} dev: false + /dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + dev: false + /dayjs@1.11.9: resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==} dev: false @@ -6556,6 +6897,12 @@ packages: resolution: {integrity: sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ==} dev: false + /delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + dependencies: + robust-predicates: 3.0.2 + dev: false + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -6742,6 +7089,10 @@ packages: resolution: {integrity: sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg==} dev: false + /dompurify@3.1.6: + resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==} + dev: false + /domutils@1.7.0: resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} dependencies: @@ -7974,6 +8325,10 @@ packages: duplexer: 0.1.2 dev: false + /hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + dev: false + /handle-thing@2.0.1: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} dev: false @@ -8487,6 +8842,11 @@ packages: has: 1.0.3 side-channel: 1.0.4 + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + /invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} dependencies: @@ -9563,10 +9923,21 @@ packages: ts.cryptojs256: 1.0.1 dev: false + /katex@0.16.11: + resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: false + /keycode@2.2.0: resolution: {integrity: sha512-ps3I9jAdNtRpJrbBvQjpzyFbss/skHqzS+eu4RxKLaEAtFqkjZaB6TZMSivPbLxf4K7VI4SjR0P5mRCX5+Q25A==} dev: false + /khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + dev: false + /kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -9594,6 +9965,17 @@ packages: resolution: {integrity: sha512-h9ivI88e1lFNmTT4HovBN33Ysn0OIJG7IPG2mkpx2uniQXFWqo35QdiX7w0TovlUFXfW8aPFblP5/q0jlOr2sA==} dev: true + /langium@3.0.0: + resolution: {integrity: sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg==} + engines: {node: '>=16.0.0'} + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + dev: false + /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} dev: false @@ -9604,6 +9986,10 @@ packages: language-subtag-registry: 0.3.22 dev: false + /layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + dev: false + /lazy-ass@1.6.0: resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} engines: {node: '> 0.8'} @@ -9823,6 +10209,12 @@ packages: resolution: {integrity: sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA==} dev: false + /marked@13.0.3: + resolution: {integrity: sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==} + engines: {node: '>= 18'} + hasBin: true + dev: false + /mathml-tag-names@2.1.3: resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} dev: true @@ -10052,6 +10444,28 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + /mermaid@11.0.2: + resolution: {integrity: sha512-KFM1o560odBHvXTTSx47ne/SE4aJKb2GbysHAVdQafIJtB6O3c0K4F+v3nC+zqS6CJhk7sXaagectNrTG+ARDw==} + dependencies: + '@braintree/sanitize-url': 7.1.0 + '@mermaid-js/parser': 0.2.0 + cytoscape: 3.30.2 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.30.2) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.10 + dayjs: 1.11.13 + dompurify: 3.1.6 + katex: 0.16.11 + khroma: 2.1.0 + lodash-es: 4.17.21 + marked: 13.0.3 + roughjs: 4.6.6 + stylis: 4.3.4 + ts-dedent: 2.2.0 + uuid: 9.0.1 + dev: false + /methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -10841,6 +11255,10 @@ packages: tslib: 2.4.0 dev: false + /path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + dev: false + /path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -10948,6 +11366,17 @@ packages: find-up: 3.0.0 dev: false + /points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + dev: false + + /points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + dev: false + /popper.js@1.16.1: resolution: {integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==} deprecated: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1 @@ -12769,6 +13198,10 @@ packages: inherits: 2.0.4 dev: false + /robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + dev: false + /rollup-plugin-terser@7.0.2(rollup@2.79.0): resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser @@ -12790,6 +13223,15 @@ packages: fsevents: 2.3.3 dev: false + /roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + dev: false + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -13604,6 +14046,10 @@ packages: resolution: {integrity: sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==} dev: false + /stylis@4.3.4: + resolution: {integrity: sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==} + dev: false + /sugarss@2.0.0: resolution: {integrity: sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==} dependencies: @@ -13920,6 +14366,11 @@ packages: resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} dev: false + /ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + dev: false + /ts-node@10.9.1(@types/node@12.20.33)(typescript@4.8.3): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true @@ -14306,6 +14757,11 @@ packages: hasBin: true dev: false + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + /uvu@0.5.6: resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} engines: {node: '>=8'} @@ -14398,10 +14854,41 @@ packages: vfile-message: 3.1.0 dev: false + /vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + dev: false + + /vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + dev: false + + /vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + dev: false + + /vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + dev: false + + /vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + dependencies: + vscode-languageserver-protocol: 3.17.5 + dev: false + /vscode-uri@3.0.3: resolution: {integrity: sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA==} dev: true + /vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + dev: false + /w3c-hr-time@1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} deprecated: Use your platform's native performance.now() and performance.timeOrigin. From ee687eee7457da22b5379acf471410cb494ba963 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Wed, 4 Sep 2024 14:07:05 +0000 Subject: [PATCH 056/111] Bot UI: Mermaid diagram gestures, save button and other improvements --- ui/cspell.json | 8 +- .../pages/Bot/Messages/Message/CodeBlock.tsx | 3 +- .../Bot/Messages/Message/MermaidDiagram.tsx | 158 +++++++++++++++++- .../Message/MermaidDiagramControls.tsx | 134 +++++++++++++++ ui/packages/platform/src/pages/Bot/utils.ts | 36 +++- 5 files changed, 330 insertions(+), 9 deletions(-) create mode 100644 ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagramControls.tsx diff --git a/ui/cspell.json b/ui/cspell.json index d7299b16..7c2ce69b 100644 --- a/ui/cspell.json +++ b/ui/cspell.json @@ -193,6 +193,12 @@ "citus", "pgvector", "partman", - "fstype" + "fstype", + "pgsql", + "sqlalchemy", + "tsql", + "TSQL", + "sparql", + "SPARQL" ] } diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx index 61b0d923..ae7a21c3 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx @@ -7,6 +7,7 @@ import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined'; import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'; import CodeIcon from '@material-ui/icons/Code'; +import { formatLanguageName } from "../../utils"; const useStyles = makeStyles((theme) => ({ container: { @@ -131,7 +132,7 @@ export const CodeBlock = memo(({ value, language }: CodeBlockProps) => { className={classes.summaryText} > - {expanded ? 'Hide' : 'Show'} code block ({codeLines.length} LOC) + {expanded ? 'Hide' : 'Show'}{language ? ` ${formatLanguageName(language)}` : ''} code block ({codeLines.length} LOC) diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx index f6ef24ae..fbf9fc63 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx @@ -1,15 +1,161 @@ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import mermaid from 'mermaid'; +import { makeStyles } from "@material-ui/core"; +import { MermaidDiagramControls } from "./MermaidDiagramControls"; +import cn from "classnames"; type MermaidDiagramProps = { chart: string } +const useStyles = makeStyles( + () => ({ + container: { + position: 'relative', + width: '100%', + overflow: 'hidden' + }, + mermaid: { + minHeight: 300, + }, + })) + +mermaid.initialize({ startOnLoad: true, er: { diagramPadding: 20, useMaxWidth: false } }); + export const MermaidDiagram = React.memo((props: MermaidDiagramProps) => { const { chart } = props; - mermaid.initialize({ startOnLoad: true }); + + const classes = useStyles(); + + // Consolidated state management + const [diagramState, setDiagramState] = useState({ + scale: 1, + position: { x: 0, y: 0 }, + dragging: false, + startPosition: { x: 0, y: 0 }, + }); + + const [isDiagramValid, setDiagramValid] = useState(null); + const [diagramError, setDiagramError] = useState(null) + + const diagramRef = useRef(null); + useEffect(() => { - mermaid.contentLoaded(); - }, [chart]); - return
{chart}
; -}) \ No newline at end of file + let isMounted = true; + if (isDiagramValid === null || chart) { + mermaid.parse(chart) + .then(() => { + if (isMounted) { + setDiagramValid(true); + mermaid.contentLoaded(); + } + }) + .catch((e) => { + if (isMounted) { + setDiagramValid(false); + setDiagramError(e.message) + console.error('Diagram contains errors:', e.message); + } + }); + } + + return () => { + isMounted = false; + }; + }, [chart, isDiagramValid]); + + const handleZoomIn = useCallback(() => { + setDiagramState((prev) => ({ + ...prev, + scale: Math.min(prev.scale + 0.1, 2), + })); + }, []); + + const handleZoomOut = useCallback(() => { + setDiagramState((prev) => ({ + ...prev, + scale: Math.max(prev.scale - 0.1, 0.8), + })); + }, []); + + const handleMouseDown = useCallback((event: React.MouseEvent) => { + setDiagramState((prev) => ({ + ...prev, + dragging: true, + startPosition: { x: event.clientX - prev.position.x, y: event.clientY - prev.position.y }, + })); + }, []); + + const handleMouseMove = useCallback((event: React.MouseEvent) => { + if (diagramState.dragging) { + setDiagramState((prev) => ({ + ...prev, + position: { x: event.clientX - prev.startPosition.x, y: event.clientY - prev.startPosition.y }, + })); + } + }, [diagramState.dragging]); + + const handleMouseUp = useCallback(() => { + setDiagramState((prev) => ({ ...prev, dragging: false })); + }, []); + + const handleTouchStart = useCallback((event: React.TouchEvent) => { + const touch = event.touches[0]; + setDiagramState((prev) => ({ + ...prev, + dragging: true, + startPosition: { x: touch.clientX - prev.position.x, y: touch.clientY - prev.position.y }, + })); + }, []); + + const handleTouchMove = useCallback((event: React.TouchEvent) => { + if (diagramState.dragging) { + const touch = event.touches[0]; + setDiagramState((prev) => ({ + ...prev, + position: { x: touch.clientX - prev.startPosition.x, y: touch.clientY - prev.startPosition.y }, + })); + } + }, [diagramState.dragging]); + + const handleTouchEnd = useCallback(() => { + setDiagramState((prev) => ({ ...prev, dragging: false })); + }, []); + + if (isDiagramValid === null) { + return

Validating diagram...

; + } + + if (isDiagramValid) { + return ( +
+
+ {chart} +
+ +
+ ); + } else { + return

{diagramError}

; + } +}); \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagramControls.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagramControls.tsx new file mode 100644 index 00000000..d947d807 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagramControls.tsx @@ -0,0 +1,134 @@ +import IconButton from "@material-ui/core/IconButton"; +import { ZoomInRounded, ZoomOutRounded, SaveAltRounded, FileCopyOutlined } from "@material-ui/icons"; +import { makeStyles } from "@material-ui/core"; +import React, { useCallback } from "react"; +import Divider from "@material-ui/core/Divider"; + +const useStyles = makeStyles( + () => ({ + container: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + + position: 'absolute', + bottom: 20, + right: 10, + zIndex: 2, + }, + controlButtons: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + + border: '1px solid rgba(0, 0, 0, 0.12)', + borderRadius: 8, + + background: 'white', + + "& .MuiIconButton-root": { + fontSize: '1.5rem', + color: 'rgba(0, 0, 0, 0.72)', + padding: 8, + '&:hover': { + color: 'rgba(0, 0, 0, 0.95)', + }, + '&:first-child': { + borderRadius: '8px 8px 0 0', + }, + '&:last-child': { + borderRadius: ' 0 0 8px 8px', + } + } + }, + divider: { + width: 'calc(100% - 8px)', + }, + actionButton: { + fontSize: '1.5rem', + color: 'rgba(0, 0, 0, 0.72)', + padding: 8, + marginBottom: 8, + '&:hover': { + color: 'rgba(0, 0, 0, 0.95)', + }, + } + })) + + +type MermaidDiagramControlsProps = { + handleZoomIn: () => void, + handleZoomOut: () => void, + diagramRef: React.RefObject, + sourceCode: string +} + +export const MermaidDiagramControls = (props: MermaidDiagramControlsProps) => { + const { sourceCode, handleZoomOut, handleZoomIn, diagramRef } = props; + const classes = useStyles(); + + const handleSaveClick = useCallback(() => { + if (diagramRef.current) { + const svgElement = diagramRef.current.querySelector('svg'); + if (svgElement) { + const svgData = new XMLSerializer().serializeToString(svgElement); + const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' }); + const url = URL.createObjectURL(svgBlob); + + const link = document.createElement('a'); + link.href = url; + link.download = 'er-diagram.svg'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + URL.revokeObjectURL(url); + } + } + }, []); + + const handleCopyClick = async () => { + if ('clipboard' in navigator) { + await navigator.clipboard.writeText(sourceCode); + } + } + + return ( +
+ + + + + + + +
+ + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/utils.ts b/ui/packages/platform/src/pages/Bot/utils.ts index d6122ad6..9e0cfe91 100644 --- a/ui/packages/platform/src/pages/Bot/utils.ts +++ b/ui/packages/platform/src/pages/Bot/utils.ts @@ -57,4 +57,38 @@ export const createMessageFragment = (messages: DebugMessage[]): DocumentFragmen }); return fragment; -}; \ No newline at end of file +}; + +export const formatLanguageName = (language: string): string => { + const specificCases: { [key: string]: string } = { + "sql": "SQL", + "pl/pgsql": "PL/pgSQL", + "pl/python": "PL/Python", + "json": "JSON", + "yaml": "YAML", + "html": "HTML", + "xml": "XML", + "css": "CSS", + "csv": "CSV", + "toml": "TOML", + "ini": "INI", + "r": "R", + "php": "PHP", + "sqlalchemy": "SQLAlchemy", + "xslt": "XSLT", + "xsd": "XSD", + "ajax": "AJAX", + "tsql": "TSQL", + "pl/sql": "PL/SQL", + "dax": "DAX", + "sparql": "SPARQL" + }; + + const normalizedLanguage = language.toLowerCase(); + + if (specificCases[normalizedLanguage]) { + return specificCases[normalizedLanguage]; + } else { + return language.charAt(0).toUpperCase() + language.slice(1).toLowerCase(); + } +} \ No newline at end of file From 956bdef57349fbdd2cdd1bdf9eb4f7ffbdb611a7 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Wed, 4 Sep 2024 14:07:33 +0000 Subject: [PATCH 057/111] Bot UI: Fix scroll during streaming --- ui/packages/platform/src/pages/Bot/Messages/Messages.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx index d2f1b5ce..25dad713 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx @@ -125,7 +125,8 @@ export const Messages = React.memo(() => { loading: isLoading, wsLoading: isWaitingForAnswer, stateMessage, - currentStreamMessage + currentStreamMessage, + isStreamingInProcess } = useAiBot(); const rootRef = useRef(null); @@ -174,10 +175,10 @@ export const Messages = React.memo(() => { }, [prevUserMessagesCount, userMessagesCount]); useEffect(() => { - if (!isLoading) { + if (!isLoading && !isStreamingInProcess) { scrollBottomIfNeed(); } - }, [isLoading, scrollBottomIfNeed]); + }, [isLoading, scrollBottomIfNeed, isStreamingInProcess]); useEffect(() => { const updateTimes = () => { From b12a07f4af509308a15d5668b4b5f052540f902b Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 5 Sep 2024 16:36:14 +0000 Subject: [PATCH 058/111] Bot UI: Display private chats within the organization --- ui/packages/platform/src/api/bot/getChats.ts | 2 +- .../src/pages/Bot/ChatsList/ChatsList.tsx | 25 ++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/ui/packages/platform/src/api/bot/getChats.ts b/ui/packages/platform/src/api/bot/getChats.ts index c1ad1c71..1a47da61 100644 --- a/ui/packages/platform/src/api/bot/getChats.ts +++ b/ui/packages/platform/src/api/bot/getChats.ts @@ -11,7 +11,7 @@ export const getChats = async (req: Req): Promise<{ response: BotMessage[] | nul const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; try { - const response = await request(`${apiServer}/chats${query ? query : ''}`, { + const response = await request(`${apiServer}/chats_auth${query ? query : ''}`, { method: 'GET', }); diff --git a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx index 9c8fc418..edbebab1 100644 --- a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx +++ b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx @@ -9,7 +9,7 @@ import React from "react"; import { Link } from "react-router-dom"; import { useParams } from "react-router"; import cn from "classnames"; -import { makeStyles, Theme, useMediaQuery } from "@material-ui/core"; +import { ListItem, ListItemIcon, makeStyles, Theme, useMediaQuery } from "@material-ui/core"; import Drawer from '@material-ui/core/Drawer'; import List from "@material-ui/core/List"; import Divider from "@material-ui/core/Divider"; @@ -17,9 +17,9 @@ import ListSubheader from '@material-ui/core/ListSubheader'; import Box from "@mui/material/Box"; import { Spinner } from "@postgres.ai/shared/components/Spinner"; import { HeaderButtons, HeaderButtonsProps } from "../HeaderButtons/HeaderButtons"; -import { BotMessage } from "../../../types/api/entities/bot"; import { theme } from "@postgres.ai/shared/styles/theme"; import { useAiBot } from "../hooks"; +import VisibilityOffIcon from '@material-ui/icons/VisibilityOff'; const useStyles = makeStyles((theme) => ({ @@ -73,13 +73,23 @@ const useStyles = makeStyles((theme) => ({ whiteSpace: 'nowrap', textDecoration: "none", flex: '0 0 2.5rem', + display: 'block', '&:hover': { background: 'rgba(0, 0, 0, 0.04)' + }, + '&:focus': { + outline: 'none', + background: 'rgba(0, 0, 0, 0.04)' } }, listItemLinkActive: { background: 'rgba(0, 0, 0, 0.04)' }, + listItemIcon: { + transform: 'translateY(2px)', + marginRight: 2, + minWidth: 'auto', + }, loader: { width: '100%', height: '100%', @@ -165,15 +175,22 @@ export const ChatsList = (props: ChatsListProps) => { const isActive = item.id === params.threadId const link = linkBuilder(item.id) return ( - handleClick(item.id)} + autoFocus={isActive} > + + {!item.is_public && } + {item.content} - + ) }) } From 89d245169493a8a2838057d2a4b785cb6d681ed2 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 6 Sep 2024 04:02:05 +0000 Subject: [PATCH 059/111] Bot UI: Mermaid controls background --- .../Message/MermaidDiagramControls.tsx | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagramControls.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagramControls.tsx index d947d807..d7d6837d 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagramControls.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagramControls.tsx @@ -44,11 +44,18 @@ const useStyles = makeStyles( divider: { width: 'calc(100% - 8px)', }, + actionButtonWrapper: { + border: '1px solid rgba(0, 0, 0, 0.12)', + borderRadius: 8, + background: 'white', + marginBottom: 8, + overflow: 'hidden' + }, actionButton: { fontSize: '1.5rem', color: 'rgba(0, 0, 0, 0.72)', padding: 8, - marginBottom: 8, + borderRadius: 0, '&:hover': { color: 'rgba(0, 0, 0, 0.95)', }, @@ -95,23 +102,26 @@ export const MermaidDiagramControls = (props: MermaidDiagramControlsProps) => { return (
- - - - - - - +
+ + + +
+
+ + + +
Date: Fri, 6 Sep 2024 04:02:31 +0000 Subject: [PATCH 060/111] Bot UI: Centring diagram on tablets and wider screens --- .../Bot/Messages/Message/MermaidDiagram.tsx | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx index fbf9fc63..a3100717 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx @@ -8,19 +8,34 @@ type MermaidDiagramProps = { chart: string } +type DiagramPosition = { + x: number, + y: number +} + +type DiagramState = { + scale: number, + position: DiagramPosition, + startPosition: DiagramPosition, + dragging: boolean +} + const useStyles = makeStyles( - () => ({ + (theme) => ({ container: { position: 'relative', width: '100%', overflow: 'hidden' }, mermaid: { - minHeight: 300, + [theme.breakpoints.up('sm')]: { + display: 'flex', + justifyContent: 'center', + } }, })) -mermaid.initialize({ startOnLoad: true, er: { diagramPadding: 20, useMaxWidth: false } }); +mermaid.initialize({ startOnLoad: true, er: { useMaxWidth: false } }); export const MermaidDiagram = React.memo((props: MermaidDiagramProps) => { const { chart } = props; @@ -28,7 +43,7 @@ export const MermaidDiagram = React.memo((props: MermaidDiagramProps) => { const classes = useStyles(); // Consolidated state management - const [diagramState, setDiagramState] = useState({ + const [diagramState, setDiagramState] = useState({ scale: 1, position: { x: 0, y: 0 }, dragging: false, @@ -134,7 +149,7 @@ export const MermaidDiagram = React.memo((props: MermaidDiagramProps) => { ref={diagramRef} style={{ transform: `scale(${diagramState.scale}) translate(${diagramState.position.x}px, ${diagramState.position.y}px)`, - transformOrigin: '0 0', + transformOrigin: '50% 50%', cursor: diagramState.dragging ? 'grabbing' : 'grab', }} onMouseDown={handleMouseDown} From 285293eaaeb61be769dd49cde76fa03aef961adf Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 13 Sep 2024 17:22:23 +0000 Subject: [PATCH 061/111] Rename "bot" to "assistant" in all text occurrences --- .../src/components/Dashboard/Dashboard.tsx | 2 +- .../src/components/IndexPage/IndexPage.tsx | 28 +++++++++++++++---- .../src/pages/Bot/ChatsList/ChatsList.tsx | 4 +-- .../pages/Bot/Messages/Message/Message.tsx | 2 +- .../src/pages/Bot/Messages/Messages.tsx | 2 +- ui/packages/platform/src/pages/Bot/hooks.tsx | 2 +- ui/packages/platform/src/pages/Bot/index.tsx | 6 ++-- ui/packages/platform/src/utils/urls.ts | 2 +- 8 files changed, 32 insertions(+), 16 deletions(-) diff --git a/ui/packages/platform/src/components/Dashboard/Dashboard.tsx b/ui/packages/platform/src/components/Dashboard/Dashboard.tsx index 6764a30a..c587878f 100644 --- a/ui/packages/platform/src/components/Dashboard/Dashboard.tsx +++ b/ui/packages/platform/src/components/Dashboard/Dashboard.tsx @@ -128,7 +128,7 @@ class Dashboard extends Component { document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname.split('.').slice(-2).join('.')}`; if (userProfile && userProfile.data && userProfile.data.orgs) { if (userProfile.data.orgs.hasOwnProperty('demo')) { - that.props.history.push(`demo/bot/${response.final_thread_id}`); + that.props.history.push(`demo/assistant/${response.final_thread_id}`); } } } diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index 5676751a..ef4f90cb 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -362,12 +362,12 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { {icons.aiBotIcon} - AI BotNEW + AI AssistantNEW } - AI Bot + AI Assistant */} ( )} /> ( )} /> + { + const { org, threadId } = props.match.params; + return ; + }} + /> + { + const { org } = props.match.params; + return ; + }} + /> ( @@ -894,7 +910,7 @@ function OrganizationWrapper(parentProps: OrganizationWrapperProps) { )} /> ( )} diff --git a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx index edbebab1..4d990e6a 100644 --- a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx +++ b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx @@ -126,9 +126,9 @@ export const ChatsList = (props: ChatsListProps) => { const matches = useMediaQuery(theme.breakpoints.down('sm')); const linkBuilder = (msgId: string) => { if (params.org) { - return `/${params.org}/bot/${msgId}` + return `/${params.org}/assistant/${msgId}` } else { - return `/bot/${msgId}` + return `/assistant/${msgId}` } } diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx index 99baab53..7cbf06cf 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx @@ -302,7 +302,7 @@ export const Message = React.memo((props: MessageProps) => { {isAi ? Postgres.AI Bot avatar : icons.userChatIcon} diff --git a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx index 25dad713..df4145c7 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx @@ -221,7 +221,7 @@ export const Messages = React.memo(() => { return (
- Postgres.AI Bot can make mistakes.
+ Postgres.AI Assistant can make mistakes.
Consider checking important information.
Depending on settings, LLM service provider such as GCP or OpenAI is used.
diff --git a/ui/packages/platform/src/pages/Bot/hooks.tsx b/ui/packages/platform/src/pages/Bot/hooks.tsx index e37961d3..5c388ac7 100644 --- a/ui/packages/platform/src/pages/Bot/hooks.tsx +++ b/ui/packages/platform/src/pages/Bot/hooks.tsx @@ -188,7 +188,7 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => if (document.visibilityState === "hidden") { if (Notification.permission === "granted") { new Notification("New message", { - body: 'New message from Postgres.AI Bot', + body: 'New message from Postgres.AI Assistant', icon: '/images/bot_avatar.png' }); } diff --git a/ui/packages/platform/src/pages/Bot/index.tsx b/ui/packages/platform/src/pages/Bot/index.tsx index aa135d31..00bad143 100644 --- a/ui/packages/platform/src/pages/Bot/index.tsx +++ b/ui/packages/platform/src/pages/Bot/index.tsx @@ -122,7 +122,7 @@ export const BotPage = (props: BotPageProps) => { org={match.params.org} project={project} breadcrumbs={[ - { name: 'Bot', url: 'bot' }, + { name: 'Assistant', url: 'assistant' }, ]} /> ); @@ -146,7 +146,7 @@ export const BotPage = (props: BotPageProps) => { const handleCreateNewChat = () => { clearChat(); - history.push(`/${match.params.org}/bot`); + history.push(`/${match.params.org}/assistant`); } const handleChatListLinkClick = (targetThreadId: string) => { @@ -158,7 +158,7 @@ export const BotPage = (props: BotPageProps) => { useEffect(() => { if (!match.params.threadId && !prevThreadId && messages && messages.length > 0 && messages[0].id) { // hack that skip additional loading chats_ancestors_and_descendants - history.replace(`/${match.params.org}/bot/${messages[0].id}`, { skipReloading: true }) + history.replace(`/${match.params.org}/assistant/${messages[0].id}`, { skipReloading: true }) getChatsList(); } else if (prevThreadId && !match.params.threadId) { clearChat() diff --git a/ui/packages/platform/src/utils/urls.ts b/ui/packages/platform/src/utils/urls.ts index 20f68e91..0527092f 100644 --- a/ui/packages/platform/src/utils/urls.ts +++ b/ui/packages/platform/src/utils/urls.ts @@ -223,6 +223,6 @@ export default { linkBotChat: function (props: PropsType, msgId: string) { const basePath = this.getBasePath(props) - return basePath + '/bot/' + msgId + return basePath + '/assistant/' + msgId }, } From 441296cb5d6e0188816a4ebe942947db46525736 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 13 Sep 2024 17:22:51 +0000 Subject: [PATCH 062/111] Bot UI: Don't render mermaid until message streaming is complete --- .../platform/src/pages/Bot/Messages/Message/Message.tsx | 7 +++++-- ui/packages/platform/src/pages/Bot/Messages/Messages.tsx | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx index 99baab53..3e82b75d 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx @@ -22,12 +22,14 @@ type BaseMessageProps = { formattedTime?: string; aiModel?: string stateMessage?: StateMessage | null + isCurrentStreamMessage?: boolean } type AiMessageProps = BaseMessageProps & { isAi: true; content: string; aiModel: string + isCurrentStreamMessage?: boolean } type HumanMessageProps = BaseMessageProps & { @@ -256,7 +258,8 @@ export const Message = React.memo((props: MessageProps) => { created_at, isLoading, aiModel, - stateMessage + stateMessage, + isCurrentStreamMessage } = props; const [isDebugVisible, setDebugVisible] = useState(false); @@ -280,7 +283,7 @@ export const Message = React.memo((props: MessageProps) => { if (!inline) { return ( <> - {matchMermaid && } + {matchMermaid && !isCurrentStreamMessage && } ) diff --git a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx index 25dad713..666cd8d7 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx @@ -281,6 +281,7 @@ export const Messages = React.memo(() => { isAi content={currentStreamMessage.content} aiModel={currentStreamMessage.ai_model} + isCurrentStreamMessage /> } {isWaitingForAnswer && From 94ddc563287a8052e41283a14ffe6ca8613a057c Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 13 Sep 2024 17:23:08 +0000 Subject: [PATCH 063/111] Bot UI: Hint cards --- ui/cspell.json | 3 +- .../ArrowGrowthIcon/ArrowGrowthIcon.tsx | 15 ++++ .../CommonTypeIcon/CommonTypeIcon.tsx | 14 ++++ .../pages/Bot/HintCards/HintCard/HintCard.tsx | 76 +++++++++++++++++++ .../src/pages/Bot/HintCards/HintCards.tsx | 36 +++++++++ .../Bot/HintCards/TableIcon/TableIcon.tsx | 19 +++++ .../Bot/HintCards/WrenchIcon/WrenchIcon.tsx | 14 ++++ .../src/pages/Bot/Messages/Messages.tsx | 6 +- ui/packages/platform/src/pages/Bot/hints.ts | 30 ++++++++ ui/packages/platform/src/pages/Bot/index.tsx | 2 +- ui/packages/platform/src/pages/Bot/utils.ts | 21 ++++- 11 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 ui/packages/platform/src/pages/Bot/HintCards/ArrowGrowthIcon/ArrowGrowthIcon.tsx create mode 100644 ui/packages/platform/src/pages/Bot/HintCards/CommonTypeIcon/CommonTypeIcon.tsx create mode 100644 ui/packages/platform/src/pages/Bot/HintCards/HintCard/HintCard.tsx create mode 100644 ui/packages/platform/src/pages/Bot/HintCards/HintCards.tsx create mode 100644 ui/packages/platform/src/pages/Bot/HintCards/TableIcon/TableIcon.tsx create mode 100644 ui/packages/platform/src/pages/Bot/HintCards/WrenchIcon/WrenchIcon.tsx create mode 100644 ui/packages/platform/src/pages/Bot/hints.ts diff --git a/ui/cspell.json b/ui/cspell.json index 7c2ce69b..857ef8f8 100644 --- a/ui/cspell.json +++ b/ui/cspell.json @@ -199,6 +199,7 @@ "tsql", "TSQL", "sparql", - "SPARQL" + "SPARQL", + "subtransactions" ] } diff --git a/ui/packages/platform/src/pages/Bot/HintCards/ArrowGrowthIcon/ArrowGrowthIcon.tsx b/ui/packages/platform/src/pages/Bot/HintCards/ArrowGrowthIcon/ArrowGrowthIcon.tsx new file mode 100644 index 00000000..6fcafec1 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/HintCards/ArrowGrowthIcon/ArrowGrowthIcon.tsx @@ -0,0 +1,15 @@ +import type { SVGProps } from 'react' +import React from 'react' + +export function ArrowGrowthIcon() { + return ( + + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/HintCards/CommonTypeIcon/CommonTypeIcon.tsx b/ui/packages/platform/src/pages/Bot/HintCards/CommonTypeIcon/CommonTypeIcon.tsx new file mode 100644 index 00000000..061f9fdb --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/HintCards/CommonTypeIcon/CommonTypeIcon.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +export const CommonTypeIcon = () => { + return ( + + + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/HintCards/HintCard/HintCard.tsx b/ui/packages/platform/src/pages/Bot/HintCards/HintCard/HintCard.tsx new file mode 100644 index 00000000..2af4818b --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/HintCards/HintCard/HintCard.tsx @@ -0,0 +1,76 @@ +import React from 'react' +import { Hint } from 'pages/Bot/hints' +import { matchHintTypeAndIcon } from "../../utils"; +import { makeStyles } from "@material-ui/core"; +import { useAiBot } from "../../hooks"; + +const useStyles = makeStyles((theme) => ({ + container: { + backgroundColor: 'transparent', + border: '1px solid rgba(0, 0, 0, 0.25)', + borderRadius: '0.5rem', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + cursor: 'pointer', + width: '11rem', + height: '6rem', + padding: '0.5rem', + color: 'black', + textAlign: 'left', + fontSize: '0.938rem', + transition: '0.2s ease-in', + + '& svg': { + width: '22px', + height: '22px', + marginBottom: '0.5rem', + '& path': { + stroke: 'black', + }, + [theme.breakpoints.down('sm')]: { + width: '16px', + height: '16px' + } + }, + + '&:hover, &:focus-visible': { + border: '1px solid rgba(0, 0, 0, 0.8)', + }, + [theme.breakpoints.down(1024)]: { + flex: '1 1 45%', + }, + [theme.breakpoints.down(480)]: { + margin: '0 0.5rem', + fontSize: '0.813rem', + height: 'auto', + }, + }, +})); + +export const HintCard = (props: Hint & {orgId: number}) => { + const { prompt, hint, type, orgId } = props; + const { sendMessage, chatVisibility } = useAiBot(); + + const classes = useStyles(); + + const handleSendMessage = async () => { + await sendMessage({ + content: prompt, + org_id: orgId, + is_public: chatVisibility === 'public' + }) + } + + return ( + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/HintCards/HintCards.tsx b/ui/packages/platform/src/pages/Bot/HintCards/HintCards.tsx new file mode 100644 index 00000000..650e522e --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/HintCards/HintCards.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { hints } from '../hints' +import { HintCard } from "./HintCard/HintCard"; +import { makeStyles } from "@material-ui/core"; + +const useStyles = makeStyles((theme) => ({ + container: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-around', + gap: '.5rem', + marginTop: '2rem', + [theme.breakpoints.down(1200)]: { + justifyContent: 'center', + }, + [theme.breakpoints.down(480)]: { + marginBottom: '1.5rem', + }, + [theme.breakpoints.down(760)]: { + '& > *:nth-child(n+3)': { + display: 'none', + }, + }, + }, +})); + +export const HintCards = React.memo(({orgId}: {orgId: number}) => { + const classes = useStyles(); + return ( +
+ { + hints.map((hint) => ) + } +
+ ) +}) \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/HintCards/TableIcon/TableIcon.tsx b/ui/packages/platform/src/pages/Bot/HintCards/TableIcon/TableIcon.tsx new file mode 100644 index 00000000..a4f2fb0e --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/HintCards/TableIcon/TableIcon.tsx @@ -0,0 +1,19 @@ +import React from 'react' + +export const TableIcon = () => { + return ( + + + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/HintCards/WrenchIcon/WrenchIcon.tsx b/ui/packages/platform/src/pages/Bot/HintCards/WrenchIcon/WrenchIcon.tsx new file mode 100644 index 00000000..ad2fa49d --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/HintCards/WrenchIcon/WrenchIcon.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +export const WrenchIcon = () => { + return ( + + + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx index 25dad713..16343875 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx @@ -14,9 +14,10 @@ import {PageSpinner} from "@postgres.ai/shared/components/PageSpinner"; import { usePrev } from 'hooks/usePrev'; import { getMaxScrollTop, getUserMessagesCount } from './utils'; import Format from "../../../utils/format"; -import { BotMessage, BotMessageWithDebugInfo } from "../../../types/api/entities/bot"; +import { BotMessage } from "../../../types/api/entities/bot"; import { Message } from "./Message/Message"; import { useAiBot } from "../hooks"; +import { HintCards } from "../HintCards/HintCards"; const useStyles = makeStyles( (theme) => ({ @@ -119,7 +120,7 @@ type FormattedTime = { [id: string]: Time } -export const Messages = React.memo(() => { +export const Messages = React.memo(({orgId}: {orgId: number}) => { const { messages, loading: isLoading, @@ -225,6 +226,7 @@ export const Messages = React.memo(() => { Consider checking important information.
Depending on settings, LLM service provider such as GCP or OpenAI is used. +
) } diff --git a/ui/packages/platform/src/pages/Bot/hints.ts b/ui/packages/platform/src/pages/Bot/hints.ts new file mode 100644 index 00000000..6ff3bcd1 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/hints.ts @@ -0,0 +1,30 @@ +export type HintType = 'design' | 'settings' | 'performance' | 'common' + +export type Hint = { + hint: string, + prompt: string, + type: HintType +} + +export const hints: Hint[] = [ + { + hint: 'Help me design an IoT system DB schema', + prompt: 'Help me design an IoT system DB schema', + type: 'design' + }, + { + hint: 'Should I enable wal_compression?', + prompt: 'Should I enable wal_compression?', + type: 'settings', + }, + { + hint: 'Demonstrate benefits of Index-Only Scans', + prompt: 'Show the benefits of Index-Only Scans. Invent a test case, create two types of queries, run them on Postgres 16, and show the plans. Then explain the difference.', + type: 'performance', + }, + { + hint: 'What do people say about subtransactions?', + prompt: 'What do people say about subtransactions?', + type: 'common' + }, +] \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/index.tsx b/ui/packages/platform/src/pages/Bot/index.tsx index aa135d31..45efbdef 100644 --- a/ui/packages/platform/src/pages/Bot/index.tsx +++ b/ui/packages/platform/src/pages/Bot/index.tsx @@ -226,7 +226,7 @@ export const BotPage = (props: BotPageProps) => { - + { const apiUrl = process.env.REACT_APP_API_URL_PREFIX || API_URL_PREFIX; @@ -91,4 +97,17 @@ export const formatLanguageName = (language: string): string => { } else { return language.charAt(0).toUpperCase() + language.slice(1).toLowerCase(); } +} + +export function matchHintTypeAndIcon(hintType: HintType): FunctionComponent { + switch (hintType) { + case 'performance': + return ArrowGrowthIcon + case 'settings': + return WrenchIcon + case 'design': + return TableIcon + default: + return CommonTypeIcon + } } \ No newline at end of file From 5ae950fe085fecbee7e6c4806246e0af86751b75 Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Wed, 18 Sep 2024 20:16:41 +0000 Subject: [PATCH 064/111] Update extended-postgres image to v0.4.6 --- README.md | 2 +- .../configs/config.example.logical_generic.yml | 2 +- .../configs/config.example.logical_rds_iam.yml | 2 +- .../configs/config.example.physical_generic.yml | 2 +- .../config.example.physical_pgbackrest.yml | 2 +- engine/configs/config.example.physical_walg.yml | 2 +- engine/test/_cleanup.sh | 2 +- translations/README.german.md | 2 +- translations/README.portuguese-br.md | 2 +- translations/README.russian.md | 2 +- translations/README.spanish.md | 2 +- translations/README.ukrainian.md | 2 +- .../shared/pages/Configuration/utils/index.ts | 16 ++++++++-------- 13 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b8273bbd..8500b0e6 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ Read more: - Theoretical max of snapshots/clones: 264 ([ZFS](https://en.wikipedia.org/wiki/ZFS), default) - Maximum size of PostgreSQL data directory: 256 quadrillion zebibytes, or 2128 bytes ([ZFS](https://en.wikipedia.org/wiki/ZFS), default) - Support & technologies - - Supported PostgreSQL versions: 9.6–15 + - Supported PostgreSQL versions: 9.6–16 - Thin cloning ([CoW](https://en.wikipedia.org/wiki/Copy-on-write)) technologies: [ZFS](https://en.wikipedia.org/wiki/ZFS) and [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)) - UI for manual tasks and API & CLI for automation - Packaged in Docker containers for all components diff --git a/engine/configs/config.example.logical_generic.yml b/engine/configs/config.example.logical_generic.yml index 94a94474..7a85bfa2 100644 --- a/engine/configs/config.example.logical_generic.yml +++ b/engine/configs/config.example.logical_generic.yml @@ -101,7 +101,7 @@ databaseContainer: &db_container # It is possible to choose any custom or official Docker image that runs Postgres. Our Dockerfile # (See https://gitlab.com/postgres-ai/custom-images/-/tree/master/extended) # is recommended in case if customization is needed. - dockerImage: "postgresai/extended-postgres:15-0.4.1" + dockerImage: "postgresai/extended-postgres:16-0.4.6" # Container parameters, see # https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources diff --git a/engine/configs/config.example.logical_rds_iam.yml b/engine/configs/config.example.logical_rds_iam.yml index bfef499f..444d246f 100644 --- a/engine/configs/config.example.logical_rds_iam.yml +++ b/engine/configs/config.example.logical_rds_iam.yml @@ -100,7 +100,7 @@ databaseContainer: &db_container # It is possible to choose any custom or official Docker image that runs Postgres. Our Dockerfile # (See https://gitlab.com/postgres-ai/custom-images/-/tree/master/extended) # is recommended in case if customization is needed. - dockerImage: "postgresai/extended-postgres:15-0.4.1" + dockerImage: "postgresai/extended-postgres:16-0.4.6" # Custom parameters for containers with PostgreSQL, see # https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources diff --git a/engine/configs/config.example.physical_generic.yml b/engine/configs/config.example.physical_generic.yml index 0e835a24..858c66bd 100644 --- a/engine/configs/config.example.physical_generic.yml +++ b/engine/configs/config.example.physical_generic.yml @@ -100,7 +100,7 @@ databaseContainer: &db_container # Any custom or official Docker image that runs Postgres. Our Dockerfile # (See https://gitlab.com/postgres-ai/custom-images/-/tree/master/extended) # is recommended in case if customization is needed. - dockerImage: "postgresai/extended-postgres:15-0.4.1" + dockerImage: "postgresai/extended-postgres:16-0.4.6" # Custom parameters for containers with PostgreSQL, see # https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources diff --git a/engine/configs/config.example.physical_pgbackrest.yml b/engine/configs/config.example.physical_pgbackrest.yml index 8be51870..04d9ed8e 100644 --- a/engine/configs/config.example.physical_pgbackrest.yml +++ b/engine/configs/config.example.physical_pgbackrest.yml @@ -100,7 +100,7 @@ databaseContainer: &db_container # Any custom or official Docker image that runs Postgres. Our Dockerfile # (See https://gitlab.com/postgres-ai/custom-images/-/tree/master/extended) # is recommended in case if customization is needed. - dockerImage: "postgresai/extended-postgres:15-0.4.1" + dockerImage: "postgresai/extended-postgres:16-0.4.6" # Custom parameters for containers with PostgreSQL, see # https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources diff --git a/engine/configs/config.example.physical_walg.yml b/engine/configs/config.example.physical_walg.yml index 2238195b..66060f7c 100644 --- a/engine/configs/config.example.physical_walg.yml +++ b/engine/configs/config.example.physical_walg.yml @@ -100,7 +100,7 @@ databaseContainer: &db_container # Any custom or official Docker image that runs Postgres. Our Dockerfile # (See https://gitlab.com/postgres-ai/custom-images/-/tree/master/extended) # is recommended in case if customization is needed. - dockerImage: "postgresai/extended-postgres:15-0.4.1" + dockerImage: "postgresai/extended-postgres:16-0.4.6" # Custom parameters for containers with PostgreSQL, see # https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources diff --git a/engine/test/_cleanup.sh b/engine/test/_cleanup.sh index b9c234a1..2800fa76 100644 --- a/engine/test/_cleanup.sh +++ b/engine/test/_cleanup.sh @@ -33,4 +33,4 @@ sudo zpool destroy test_dblab_pool \ sudo rm -f "${ZFS_FILE}" # Remove CLI configuration -dblab config remove test || { echo "Cannot remove CLI configuration but this was optional (ignore the error)."; } +dblab config remove test || true diff --git a/translations/README.german.md b/translations/README.german.md index 069a46b1..7e805df7 100644 --- a/translations/README.german.md +++ b/translations/README.german.md @@ -80,7 +80,7 @@ Weiterlesen: - Blitzschnelles Klonen von Postgres-Datenbanken. Es wird ein paar Sekunden gebraucht, um einen neuen Klon zu erstellen, der bereit ist, Verbindungen und Abfragen zu akzeptieren, unabhängig von der Datenbankgröße. - Die theoretische maximale Anzahl von Snapshots und Klonen beträgt 264 ([ZFS](https://en.wikipedia.org/wiki/ZFS), Standard). - Theoretische maximale Größe des PostgreSQL-Datenverzeichnisses: 256 Billiarden Zebibyte oder 2128 Byte ([ZFS](https://en.wikipedia.org/wiki/ZFS), Standard). -- Unterstützte Hauptversionen von PostgreSQL: 9.6–14. +- Unterstützte Hauptversionen von PostgreSQL: 9.6–16. - Zwei Technologien werden unterstützt, um Thin Cloning zu ermöglichen ([CoW](https://en.wikipedia.org/wiki/Copy-on-write)): [ZFS](https://en.wikipedia.org/wiki/ ZFS) und [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)). - Alle Komponenten sind in Docker-Containern verpackt. - UI macht die manuelle Arbeit bequemer. diff --git a/translations/README.portuguese-br.md b/translations/README.portuguese-br.md index 2e68bf8a..1ab510c9 100644 --- a/translations/README.portuguese-br.md +++ b/translations/README.portuguese-br.md @@ -80,7 +80,7 @@ Leia mais: - Clonagem the bancos de dados Postgres ultrarrápidos - apenas alguns segundos para criar um novo clone pronto para aceitar conexões e queries, independentemente do tamanho do banco de dados. - O número máximo teórico de snapshots e clones é 264 ([ZFS](https://en.wikipedia.org/wiki/ZFS), default). - O número máximo teórico de do diretório de dados do PostgreSQL: 256 quatrilhões zebibytes, ou 2128 bytes ([ZFS](https://en.wikipedia.org/wiki/ZFS), default). -- Versões _major_ do PostgreSQL suportadas: 9.6–14. +- Versões _major_ do PostgreSQL suportadas: 9.6–16. - Duas tecnologias são suportadas para viabilizar o thin cloning ([CoW](https://en.wikipedia.org/wiki/Copy-on-write)): [ZFS](https://en.wikipedia.org/wiki/ZFS) e [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)). - Todos os componentes estão empacotados em docker containers. - UI para tornar o trabalho manual mais conveniente. diff --git a/translations/README.russian.md b/translations/README.russian.md index 61d71325..ad2e4733 100644 --- a/translations/README.russian.md +++ b/translations/README.russian.md @@ -81,7 +81,7 @@ - Молниеносное клонирование БД Postgres - создание нового клона, готового к работе, всего за несколько секунд (вне зависимости от размера БД). - Максимальное теоретическое количество снимков: 264. ([ZFS](https://en.wikipedia.org/wiki/ZFS), вариант по умолчанию). - Максимальный теоретический размер директории данных PostgreSQL: 256 квадриллионов зебибайт или 2128 байт ([ZFS](https://en.wikipedia.org/wiki/ZFS), вариант по умолчанию). -- Поддерживаются все основные версии PostgreSQL: 9.6-14. +- Поддерживаются все основные версии PostgreSQL: 9.6-16. - Для реализации тонкого клонирования поддерживаются две технологии ([CoW](https://en.wikipedia.org/wiki/Copy-on-write)): [ZFS](https://en.wikipedia.org/wiki/ZFS) и [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)). - Все компоненты работают в Docker-контейнерах. - UI для удобства ручных действий пользователя. diff --git a/translations/README.spanish.md b/translations/README.spanish.md index 62ddc4bd..10719d0f 100644 --- a/translations/README.spanish.md +++ b/translations/README.spanish.md @@ -80,7 +80,7 @@ Lee más: - Clonación ultrarrápida de bases de datos de Postgres: unos segundos para crear un nuevo clon listo para aceptar conexiones y consultas, independientemente del tamaño de la base de datos. - El número máximo teórico de instantáneas y clones es 264 ([ZFS](https://en.wikipedia.org/wiki/ZFS), predeterminado). - El tamaño máximo teórico del directorio de datos de PostgreSQL: 256 cuatrillones de zebibytes, o 2128 bytes ([ZFS](https://en.wikipedia.org/wiki/ZFS), predeterminado). -- Versiones principales de PostgreSQL admitidas: 9.6–14. +- Versiones principales de PostgreSQL admitidas: 9.6–16. - Se admiten dos tecnologías para permitir la clonación ligera ([CoW](https://en.wikipedia.org/wiki/Copy-on-write)): [ZFS](https://en.wikipedia.org/wiki/ZFS) y [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)). - Todos los componentes están empaquetados en contenedores Docker. - Interfaz de usuario para que el trabajo manual sea más conveniente. diff --git a/translations/README.ukrainian.md b/translations/README.ukrainian.md index a7f6b682..e3d7dbf1 100644 --- a/translations/README.ukrainian.md +++ b/translations/README.ukrainian.md @@ -81,7 +81,7 @@ - блискавичне клонування БД Postgres - створення нового клону, готового до роботи, всього за кілька секунд (незалежно від розміру БД). - Максимальна теоретична кількість знімків: 264. ([ZFS](https://en.wikipedia.org/wiki/ZFS), варіант за замовчуванням). - Максимальний теоретичний розмір директорії даних PostgreSQL: 256 квадрильйонів зебібайт або 2128 байт ([ZFS](https://en.wikipedia.org/wiki/ZFS), варіант за замовчуванням). -- Підтримуються усі основні версії PostgreSQL: 9.6-14. +- Підтримуються усі основні версії PostgreSQL: 9.6-16. - Для реалізації тонкого клонування підтримуються дві технології ([CoW](https://en.wikipedia.org/wiki/Copy-on-write)): [ZFS](https://en.wikipedia.org/wiki/ZFS ) та [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)). - Усі компоненти працюють у Docker-контейнерах. - UI для зручності ручних дій користувача. diff --git a/ui/packages/shared/pages/Configuration/utils/index.ts b/ui/packages/shared/pages/Configuration/utils/index.ts index f671631e..27c732fd 100644 --- a/ui/packages/shared/pages/Configuration/utils/index.ts +++ b/ui/packages/shared/pages/Configuration/utils/index.ts @@ -7,14 +7,14 @@ const seContainerRegistry = 'se-images' const genericImagePrefix = 'postgresai/extended-postgres' // since some tags are rc, we need to specify the exact tags to use const dockerImagesConfig = { - '9.6': ['0.4.4', '0.4.3'], - '10': ['0.4.4', '0.4.3'], - '11': ['0.4.4', '0.4.3'], - '12': ['0.4.4', '0.4.3'], - '13': ['0.4.4', '0.4.3'], - '14': ['0.4.4', '0.4.3'], - '15': ['0.4.4', '0.4.3'], - '16': ['0.4.4', '0.4.3'], + '9.6': ['0.4.6', '0.4.5', '0.4.4'], + '10': ['0.4.6', '0.4.5', '0.4.4'], + '11': ['0.4.6', '0.4.5', '0.4.4'], + '12': ['0.4.6', '0.4.5', '0.4.4'], + '13': ['0.4.6', '0.4.5', '0.4.4'], + '14': ['0.4.6', '0.4.5', '0.4.4'], + '15': ['0.4.6', '0.4.5', '0.4.4'], + '16': ['0.4.6', '0.4.5', '0.4.4'], } export type FormValuesKey = keyof FormValues From cccc9993b56e8b0b5ac5b872ec1a8acef8fff6c2 Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Thu, 19 Sep 2024 20:47:23 +0000 Subject: [PATCH 065/111] DBLab: PostgreSQL 17 support --- README.md | 2 +- engine/.gitlab-ci.yml | 7 +- .../standard/postgres/default/17/pg_hba.conf | 128 +++ .../17/postgresql.dblab.postgresql.conf | 844 ++++++++++++++++++ engine/test/1.synthetic.sh | 4 - engine/test/2.logical_generic.sh | 2 - engine/test/4.physical_basebackup.sh | 2 - engine/test/_cleanup.sh | 18 +- translations/README.german.md | 2 +- translations/README.portuguese-br.md | 2 +- translations/README.russian.md | 2 +- translations/README.spanish.md | 2 +- translations/README.ukrainian.md | 2 +- .../shared/pages/Configuration/utils/index.ts | 17 +- 14 files changed, 1005 insertions(+), 29 deletions(-) create mode 100644 engine/configs/standard/postgres/default/17/pg_hba.conf create mode 100644 engine/configs/standard/postgres/default/17/postgresql.dblab.postgresql.conf diff --git a/README.md b/README.md index 8500b0e6..6b5bbc02 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ Read more: - Theoretical max of snapshots/clones: 264 ([ZFS](https://en.wikipedia.org/wiki/ZFS), default) - Maximum size of PostgreSQL data directory: 256 quadrillion zebibytes, or 2128 bytes ([ZFS](https://en.wikipedia.org/wiki/ZFS), default) - Support & technologies - - Supported PostgreSQL versions: 9.6–16 + - Supported PostgreSQL versions: 9.6–17 - Thin cloning ([CoW](https://en.wikipedia.org/wiki/Copy-on-write)) technologies: [ZFS](https://en.wikipedia.org/wiki/ZFS) and [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)) - UI for manual tasks and API & CLI for automation - Packaged in Docker containers for all components diff --git a/engine/.gitlab-ci.yml b/engine/.gitlab-ci.yml index cdf29be2..94cc6271 100644 --- a/engine/.gitlab-ci.yml +++ b/engine/.gitlab-ci.yml @@ -467,7 +467,12 @@ bash-test-15: bash-test-16: <<: *bash_test variables: - POSTGRES_VERSION: 16rc1 + POSTGRES_VERSION: 16 + +bash-test-17: + <<: *bash_test + variables: + POSTGRES_VERSION: 17rc1 integration-test: services: diff --git a/engine/configs/standard/postgres/default/17/pg_hba.conf b/engine/configs/standard/postgres/default/17/pg_hba.conf new file mode 100644 index 00000000..7f379dbb --- /dev/null +++ b/engine/configs/standard/postgres/default/17/pg_hba.conf @@ -0,0 +1,128 @@ +# PostgreSQL Client Authentication Configuration File +# =================================================== +# +# Refer to the "Client Authentication" section in the PostgreSQL +# documentation for a complete description of this file. A short +# synopsis follows. +# +# ---------------------- +# Authentication Records +# ---------------------- +# +# This file controls: which hosts are allowed to connect, how clients +# are authenticated, which PostgreSQL user names they can use, which +# databases they can access. Records take one of these forms: +# +# local DATABASE USER METHOD [OPTIONS] +# host DATABASE USER ADDRESS METHOD [OPTIONS] +# hostssl DATABASE USER ADDRESS METHOD [OPTIONS] +# hostnossl DATABASE USER ADDRESS METHOD [OPTIONS] +# hostgssenc DATABASE USER ADDRESS METHOD [OPTIONS] +# hostnogssenc DATABASE USER ADDRESS METHOD [OPTIONS] +# +# (The uppercase items must be replaced by actual values.) +# +# The first field is the connection type: +# - "local" is a Unix-domain socket +# - "host" is a TCP/IP socket (encrypted or not) +# - "hostssl" is a TCP/IP socket that is SSL-encrypted +# - "hostnossl" is a TCP/IP socket that is not SSL-encrypted +# - "hostgssenc" is a TCP/IP socket that is GSSAPI-encrypted +# - "hostnogssenc" is a TCP/IP socket that is not GSSAPI-encrypted +# +# DATABASE can be "all", "sameuser", "samerole", "replication", a +# database name, a regular expression (if it starts with a slash (/)) +# or a comma-separated list thereof. The "all" keyword does not match +# "replication". Access to replication must be enabled in a separate +# record (see example below). +# +# USER can be "all", a user name, a group name prefixed with "+", a +# regular expression (if it starts with a slash (/)) or a comma-separated +# list thereof. In both the DATABASE and USER fields you can also write +# a file name prefixed with "@" to include names from a separate file. +# +# ADDRESS specifies the set of hosts the record matches. It can be a +# host name, or it is made up of an IP address and a CIDR mask that is +# an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that +# specifies the number of significant bits in the mask. A host name +# that starts with a dot (.) matches a suffix of the actual host name. +# Alternatively, you can write an IP address and netmask in separate +# columns to specify the set of hosts. Instead of a CIDR-address, you +# can write "samehost" to match any of the server's own IP addresses, +# or "samenet" to match any address in any subnet that the server is +# directly connected to. +# +# METHOD can be "trust", "reject", "md5", "password", "scram-sha-256", +# "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert". +# Note that "password" sends passwords in clear text; "md5" or +# "scram-sha-256" are preferred since they send encrypted passwords. +# +# OPTIONS are a set of options for the authentication in the format +# NAME=VALUE. The available options depend on the different +# authentication methods -- refer to the "Client Authentication" +# section in the documentation for a list of which options are +# available for which authentication methods. +# +# Database and user names containing spaces, commas, quotes and other +# special characters must be quoted. Quoting one of the keywords +# "all", "sameuser", "samerole" or "replication" makes the name lose +# its special character, and just match a database or username with +# that name. +# +# --------------- +# Include Records +# --------------- +# +# This file allows the inclusion of external files or directories holding +# more records, using the following keywords: +# +# include FILE +# include_if_exists FILE +# include_dir DIRECTORY +# +# FILE is the file name to include, and DIR is the directory name containing +# the file(s) to include. Any file in a directory will be loaded if suffixed +# with ".conf". The files of a directory are ordered by name. +# include_if_exists ignores missing files. FILE and DIRECTORY can be +# specified as a relative or an absolute path, and can be double-quoted if +# they contain spaces. +# +# ------------- +# Miscellaneous +# ------------- +# +# This file is read on server startup and when the server receives a +# SIGHUP signal. If you edit the file on a running system, you have to +# SIGHUP the server for the changes to take effect, run "pg_ctl reload", +# or execute "SELECT pg_reload_conf()". +# +# ---------------------------------- +# Put your actual configuration here +# ---------------------------------- +# +# If you want to allow non-local connections, you need to add more +# "host" records. In that case you will also need to make PostgreSQL +# listen on a non-local interface via the listen_addresses +# configuration parameter, or via the -i or -h command line switches. + +# CAUTION: Configuring the system for local "trust" authentication +# allows any local user to connect as any PostgreSQL user, including +# the database superuser. If you do not trust all your local users, +# use another authentication method. + + +# TYPE DATABASE USER ADDRESS METHOD + +# "local" is for Unix domain socket connections only +local all all trust +# IPv4 local connections: +host all all 127.0.0.1/32 trust +# IPv6 local connections: +host all all ::1/128 trust +# Allow replication connections from localhost, by a user with the +# replication privilege. +local replication all trust +host replication all 127.0.0.1/32 trust +host replication all ::1/128 trust + +host all all all scram-sha-256 diff --git a/engine/configs/standard/postgres/default/17/postgresql.dblab.postgresql.conf b/engine/configs/standard/postgres/default/17/postgresql.dblab.postgresql.conf new file mode 100644 index 00000000..98e4a16e --- /dev/null +++ b/engine/configs/standard/postgres/default/17/postgresql.dblab.postgresql.conf @@ -0,0 +1,844 @@ +# ----------------------------- +# PostgreSQL configuration file +# ----------------------------- +# +# This file consists of lines of the form: +# +# name = value +# +# (The "=" is optional.) Whitespace may be used. Comments are introduced with +# "#" anywhere on a line. The complete list of parameter names and allowed +# values can be found in the PostgreSQL documentation. +# +# The commented-out settings shown in this file represent the default values. +# Re-commenting a setting is NOT sufficient to revert it to the default value; +# you need to reload the server. +# +# This file is read on server startup and when the server receives a SIGHUP +# signal. If you edit the file on a running system, you have to SIGHUP the +# server for the changes to take effect, run "pg_ctl reload", or execute +# "SELECT pg_reload_conf()". Some parameters, which are marked below, +# require a server shutdown and restart to take effect. +# +# Any parameter can also be given as a command-line option to the server, e.g., +# "postgres -c log_connections=on". Some parameters can be changed at run time +# with the "SET" SQL command. +# +# Memory units: B = bytes Time units: us = microseconds +# kB = kilobytes ms = milliseconds +# MB = megabytes s = seconds +# GB = gigabytes min = minutes +# TB = terabytes h = hours +# d = days + + +#------------------------------------------------------------------------------ +# FILE LOCATIONS +#------------------------------------------------------------------------------ + +# The default values of these variables are driven from the -D command-line +# option or PGDATA environment variable, represented here as ConfigDir. + +#data_directory = 'ConfigDir' # use data in another directory + # (change requires restart) +#hba_file = 'ConfigDir/pg_hba.conf' # host-based authentication file + # (change requires restart) +#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file + # (change requires restart) + +# If external_pid_file is not explicitly set, no extra PID file is written. +#external_pid_file = '' # write an extra PID file + # (change requires restart) + + +#------------------------------------------------------------------------------ +# CONNECTIONS AND AUTHENTICATION +#------------------------------------------------------------------------------ + +# - Connection Settings - + +listen_addresses = '*' + # comma-separated list of addresses; + # defaults to 'localhost'; use '*' for all + # (change requires restart) +#port = 5432 # (change requires restart) +max_connections = 100 # (change requires restart) +#reserved_connections = 0 # (change requires restart) +#superuser_reserved_connections = 3 # (change requires restart) +#unix_socket_directories = '/var/run/postgresql' # comma-separated list of directories + # (change requires restart) +#unix_socket_group = '' # (change requires restart) +#unix_socket_permissions = 0777 # begin with 0 to use octal notation + # (change requires restart) +#bonjour = off # advertise server via Bonjour + # (change requires restart) +#bonjour_name = '' # defaults to the computer name + # (change requires restart) + +# - TCP settings - +# see "man tcp" for details + +#tcp_keepalives_idle = 0 # TCP_KEEPIDLE, in seconds; + # 0 selects the system default +#tcp_keepalives_interval = 0 # TCP_KEEPINTVL, in seconds; + # 0 selects the system default +#tcp_keepalives_count = 0 # TCP_KEEPCNT; + # 0 selects the system default +#tcp_user_timeout = 0 # TCP_USER_TIMEOUT, in milliseconds; + # 0 selects the system default + +#client_connection_check_interval = 0 # time between checks for client + # disconnection while running queries; + # 0 for never + +# - Authentication - + +#authentication_timeout = 1min # 1s-600s +#password_encryption = scram-sha-256 # scram-sha-256 or md5 +#scram_iterations = 4096 + +# GSSAPI using Kerberos +#krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab' +#krb_caseins_users = off +#gss_accept_delegation = off + +# - SSL - + +#ssl = off +#ssl_ca_file = '' +#ssl_cert_file = 'server.crt' +#ssl_crl_file = '' +#ssl_crl_dir = '' +#ssl_key_file = 'server.key' +#ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers +#ssl_prefer_server_ciphers = on +#ssl_ecdh_curve = 'prime256v1' +#ssl_min_protocol_version = 'TLSv1.2' +#ssl_max_protocol_version = '' +#ssl_dh_params_file = '' +#ssl_passphrase_command = '' +#ssl_passphrase_command_supports_reload = off + + +#------------------------------------------------------------------------------ +# RESOURCE USAGE (except WAL) +#------------------------------------------------------------------------------ + +# - Memory - + +shared_buffers = 128MB # min 128kB + # (change requires restart) +#huge_pages = try # on, off, or try + # (change requires restart) +#huge_page_size = 0 # zero for system default + # (change requires restart) +#temp_buffers = 8MB # min 800kB +#max_prepared_transactions = 0 # zero disables the feature + # (change requires restart) +# Caution: it is not advisable to set max_prepared_transactions nonzero unless +# you actively intend to use prepared transactions. +#work_mem = 4MB # min 64kB +#hash_mem_multiplier = 2.0 # 1-1000.0 multiplier on hash table work_mem +#maintenance_work_mem = 64MB # min 64kB +#autovacuum_work_mem = -1 # min 64kB, or -1 to use maintenance_work_mem +#logical_decoding_work_mem = 64MB # min 64kB +#max_stack_depth = 2MB # min 100kB +#shared_memory_type = mmap # the default is the first option + # supported by the operating system: + # mmap + # sysv + # windows + # (change requires restart) +dynamic_shared_memory_type = posix # the default is usually the first option + # supported by the operating system: + # posix + # sysv + # windows + # mmap + # (change requires restart) +#min_dynamic_shared_memory = 0MB # (change requires restart) +#vacuum_buffer_usage_limit = 2MB # size of vacuum and analyze buffer access strategy ring; + # 0 to disable vacuum buffer access strategy; + # range 128kB to 16GB + +# SLRU buffers (change requires restart) +#commit_timestamp_buffers = 0 # memory for pg_commit_ts (0 = auto) +#multixact_offset_buffers = 16 # memory for pg_multixact/offsets +#multixact_member_buffers = 32 # memory for pg_multixact/members +#notify_buffers = 16 # memory for pg_notify +#serializable_buffers = 32 # memory for pg_serial +#subtransaction_buffers = 0 # memory for pg_subtrans (0 = auto) +#transaction_buffers = 0 # memory for pg_xact (0 = auto) + +# - Disk - + +#temp_file_limit = -1 # limits per-process temp file space + # in kilobytes, or -1 for no limit + +#max_notify_queue_pages = 1048576 # limits the number of SLRU pages allocated + # for NOTIFY / LISTEN queue + +# - Kernel Resources - + +#max_files_per_process = 1000 # min 64 + # (change requires restart) + +# - Cost-Based Vacuum Delay - + +#vacuum_cost_delay = 0 # 0-100 milliseconds (0 disables) +#vacuum_cost_page_hit = 1 # 0-10000 credits +#vacuum_cost_page_miss = 2 # 0-10000 credits +#vacuum_cost_page_dirty = 20 # 0-10000 credits +#vacuum_cost_limit = 200 # 1-10000 credits + +# - Background Writer - + +#bgwriter_delay = 200ms # 10-10000ms between rounds +#bgwriter_lru_maxpages = 100 # max buffers written/round, 0 disables +#bgwriter_lru_multiplier = 2.0 # 0-10.0 multiplier on buffers scanned/round +#bgwriter_flush_after = 512kB # measured in pages, 0 disables + +# - Asynchronous Behavior - + +#backend_flush_after = 0 # measured in pages, 0 disables +#effective_io_concurrency = 1 # 1-1000; 0 disables prefetching +#maintenance_io_concurrency = 10 # 1-1000; 0 disables prefetching +#io_combine_limit = 128kB # usually 1-32 blocks (depends on OS) +#max_worker_processes = 8 # (change requires restart) +#max_parallel_workers_per_gather = 2 # limited by max_parallel_workers +#max_parallel_maintenance_workers = 2 # limited by max_parallel_workers +#max_parallel_workers = 8 # number of max_worker_processes that + # can be used in parallel operations +#parallel_leader_participation = on + + +#------------------------------------------------------------------------------ +# WRITE-AHEAD LOG +#------------------------------------------------------------------------------ + +# - Settings - + +#wal_level = replica # minimal, replica, or logical + # (change requires restart) +#fsync = on # flush data to disk for crash safety + # (turning this off can cause + # unrecoverable data corruption) +#synchronous_commit = on # synchronization level; + # off, local, remote_write, remote_apply, or on +#wal_sync_method = fsync # the default is the first option + # supported by the operating system: + # open_datasync + # fdatasync (default on Linux and FreeBSD) + # fsync + # fsync_writethrough + # open_sync +#full_page_writes = on # recover from partial page writes +#wal_log_hints = off # also do full page writes of non-critical updates + # (change requires restart) +#wal_compression = off # enables compression of full-page writes; + # off, pglz, lz4, zstd, or on +#wal_init_zero = on # zero-fill new WAL files +#wal_recycle = on # recycle WAL files +#wal_buffers = -1 # min 32kB, -1 sets based on shared_buffers + # (change requires restart) +#wal_writer_delay = 200ms # 1-10000 milliseconds +#wal_writer_flush_after = 1MB # measured in pages, 0 disables +#wal_skip_threshold = 2MB + +#commit_delay = 0 # range 0-100000, in microseconds +#commit_siblings = 5 # range 1-1000 + +# - Checkpoints - + +#checkpoint_timeout = 5min # range 30s-1d +#checkpoint_completion_target = 0.9 # checkpoint target duration, 0.0 - 1.0 +#checkpoint_flush_after = 256kB # measured in pages, 0 disables +#checkpoint_warning = 30s # 0 disables +max_wal_size = 1GB +min_wal_size = 80MB + +# - Prefetching during recovery - + +#recovery_prefetch = try # prefetch pages referenced in the WAL? +#wal_decode_buffer_size = 512kB # lookahead window used for prefetching + # (change requires restart) + +# - Archiving - + +#archive_mode = off # enables archiving; off, on, or always + # (change requires restart) +#archive_library = '' # library to use to archive a WAL file + # (empty string indicates archive_command should + # be used) +#archive_command = '' # command to use to archive a WAL file + # placeholders: %p = path of file to archive + # %f = file name only + # e.g. 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f' +#archive_timeout = 0 # force a WAL file switch after this + # number of seconds; 0 disables + +# - Archive Recovery - + +# These are only used in recovery mode. + +#restore_command = '' # command to use to restore an archived WAL file + # placeholders: %p = path of file to restore + # %f = file name only + # e.g. 'cp /mnt/server/archivedir/%f %p' +#archive_cleanup_command = '' # command to execute at every restartpoint +#recovery_end_command = '' # command to execute at completion of recovery + +# - Recovery Target - + +# Set these only when performing a targeted recovery. + +#recovery_target = '' # 'immediate' to end recovery as soon as a + # consistent state is reached + # (change requires restart) +#recovery_target_name = '' # the named restore point to which recovery will proceed + # (change requires restart) +#recovery_target_time = '' # the time stamp up to which recovery will proceed + # (change requires restart) +#recovery_target_xid = '' # the transaction ID up to which recovery will proceed + # (change requires restart) +#recovery_target_lsn = '' # the WAL LSN up to which recovery will proceed + # (change requires restart) +#recovery_target_inclusive = on # Specifies whether to stop: + # just after the specified recovery target (on) + # just before the recovery target (off) + # (change requires restart) +#recovery_target_timeline = 'latest' # 'current', 'latest', or timeline ID + # (change requires restart) +#recovery_target_action = 'pause' # 'pause', 'promote', 'shutdown' + # (change requires restart) + +# - WAL Summarization - + +#summarize_wal = off # run WAL summarizer process? +#wal_summary_keep_time = '10d' # when to remove old summary files, 0 = never + + +#------------------------------------------------------------------------------ +# REPLICATION +#------------------------------------------------------------------------------ + +# - Sending Servers - + +# Set these on the primary and on any standby that will send replication data. + +#max_wal_senders = 10 # max number of walsender processes + # (change requires restart) +#max_replication_slots = 10 # max number of replication slots + # (change requires restart) +#wal_keep_size = 0 # in megabytes; 0 disables +#max_slot_wal_keep_size = -1 # in megabytes; -1 disables +#wal_sender_timeout = 60s # in milliseconds; 0 disables +#track_commit_timestamp = off # collect timestamp of transaction commit + # (change requires restart) + +# - Primary Server - + +# These settings are ignored on a standby server. + +#synchronous_standby_names = '' # standby servers that provide sync rep + # method to choose sync standbys, number of sync standbys, + # and comma-separated list of application_name + # from standby(s); '*' = all +#synchronized_standby_slots = '' # streaming replication standby server slot + # names that logical walsender processes will wait for + +# - Standby Servers - + +# These settings are ignored on a primary server. + +#primary_conninfo = '' # connection string to sending server +#primary_slot_name = '' # replication slot on sending server +#hot_standby = on # "off" disallows queries during recovery + # (change requires restart) +#max_standby_archive_delay = 30s # max delay before canceling queries + # when reading WAL from archive; + # -1 allows indefinite delay +#max_standby_streaming_delay = 30s # max delay before canceling queries + # when reading streaming WAL; + # -1 allows indefinite delay +#wal_receiver_create_temp_slot = off # create temp slot if primary_slot_name + # is not set +#wal_receiver_status_interval = 10s # send replies at least this often + # 0 disables +#hot_standby_feedback = off # send info from standby to prevent + # query conflicts +#wal_receiver_timeout = 60s # time that receiver waits for + # communication from primary + # in milliseconds; 0 disables +#wal_retrieve_retry_interval = 5s # time to wait before retrying to + # retrieve WAL after a failed attempt +#recovery_min_apply_delay = 0 # minimum delay for applying changes during recovery +#sync_replication_slots = off # enables slot synchronization on the physical standby from the primary + +# - Subscribers - + +# These settings are ignored on a publisher. + +#max_logical_replication_workers = 4 # taken from max_worker_processes + # (change requires restart) +#max_sync_workers_per_subscription = 2 # taken from max_logical_replication_workers +#max_parallel_apply_workers_per_subscription = 2 # taken from max_logical_replication_workers + + +#------------------------------------------------------------------------------ +# QUERY TUNING +#------------------------------------------------------------------------------ + +# - Planner Method Configuration - + +#enable_async_append = on +#enable_bitmapscan = on +#enable_gathermerge = on +#enable_hashagg = on +#enable_hashjoin = on +#enable_incremental_sort = on +#enable_indexscan = on +#enable_indexonlyscan = on +#enable_material = on +#enable_memoize = on +#enable_mergejoin = on +#enable_nestloop = on +#enable_parallel_append = on +#enable_parallel_hash = on +#enable_partition_pruning = on +#enable_partitionwise_join = off +#enable_partitionwise_aggregate = off +#enable_presorted_aggregate = on +#enable_seqscan = on +#enable_sort = on +#enable_tidscan = on +#enable_group_by_reordering = on + +# - Planner Cost Constants - + +#seq_page_cost = 1.0 # measured on an arbitrary scale +#random_page_cost = 4.0 # same scale as above +#cpu_tuple_cost = 0.01 # same scale as above +#cpu_index_tuple_cost = 0.005 # same scale as above +#cpu_operator_cost = 0.0025 # same scale as above +#parallel_setup_cost = 1000.0 # same scale as above +#parallel_tuple_cost = 0.1 # same scale as above +#min_parallel_table_scan_size = 8MB +#min_parallel_index_scan_size = 512kB +#effective_cache_size = 4GB + +#jit_above_cost = 100000 # perform JIT compilation if available + # and query more expensive than this; + # -1 disables +#jit_inline_above_cost = 500000 # inline small functions if query is + # more expensive than this; -1 disables +#jit_optimize_above_cost = 500000 # use expensive JIT optimizations if + # query is more expensive than this; + # -1 disables + +# - Genetic Query Optimizer - + +#geqo = on +#geqo_threshold = 12 +#geqo_effort = 5 # range 1-10 +#geqo_pool_size = 0 # selects default based on effort +#geqo_generations = 0 # selects default based on effort +#geqo_selection_bias = 2.0 # range 1.5-2.0 +#geqo_seed = 0.0 # range 0.0-1.0 + +# - Other Planner Options - + +#default_statistics_target = 100 # range 1-10000 +#constraint_exclusion = partition # on, off, or partition +#cursor_tuple_fraction = 0.1 # range 0.0-1.0 +#from_collapse_limit = 8 +#jit = on # allow JIT compilation +#join_collapse_limit = 8 # 1 disables collapsing of explicit + # JOIN clauses +#plan_cache_mode = auto # auto, force_generic_plan or + # force_custom_plan +#recursive_worktable_factor = 10.0 # range 0.001-1000000 + + +#------------------------------------------------------------------------------ +# REPORTING AND LOGGING +#------------------------------------------------------------------------------ + +# - Where to Log - + +#log_destination = 'stderr' # Valid values are combinations of + # stderr, csvlog, jsonlog, syslog, and + # eventlog, depending on platform. + # csvlog and jsonlog require + # logging_collector to be on. + +# This is used when logging to stderr: +#logging_collector = off # Enable capturing of stderr, jsonlog, + # and csvlog into log files. Required + # to be on for csvlogs and jsonlogs. + # (change requires restart) + +# These are only used if logging_collector is on: +#log_directory = 'log' # directory where log files are written, + # can be absolute or relative to PGDATA +#log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' # log file name pattern, + # can include strftime() escapes +#log_file_mode = 0600 # creation mode for log files, + # begin with 0 to use octal notation +#log_rotation_age = 1d # Automatic rotation of logfiles will + # happen after that time. 0 disables. +#log_rotation_size = 10MB # Automatic rotation of logfiles will + # happen after that much log output. + # 0 disables. +#log_truncate_on_rotation = off # If on, an existing log file with the + # same name as the new log file will be + # truncated rather than appended to. + # But such truncation only occurs on + # time-driven rotation, not on restarts + # or size-driven rotation. Default is + # off, meaning append to existing files + # in all cases. + +# These are relevant when logging to syslog: +#syslog_facility = 'LOCAL0' +#syslog_ident = 'postgres' +#syslog_sequence_numbers = on +#syslog_split_messages = on + +# This is only relevant when logging to eventlog (Windows): +# (change requires restart) +#event_source = 'PostgreSQL' + +# - When to Log - + +#log_min_messages = warning # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # info + # notice + # warning + # error + # log + # fatal + # panic + +#log_min_error_statement = error # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # info + # notice + # warning + # error + # log + # fatal + # panic (effectively off) + +#log_min_duration_statement = -1 # -1 is disabled, 0 logs all statements + # and their durations, > 0 logs only + # statements running at least this number + # of milliseconds + +#log_min_duration_sample = -1 # -1 is disabled, 0 logs a sample of statements + # and their durations, > 0 logs only a sample of + # statements running at least this number + # of milliseconds; + # sample fraction is determined by log_statement_sample_rate + +#log_statement_sample_rate = 1.0 # fraction of logged statements exceeding + # log_min_duration_sample to be logged; + # 1.0 logs all such statements, 0.0 never logs + + +#log_transaction_sample_rate = 0.0 # fraction of transactions whose statements + # are logged regardless of their duration; 1.0 logs all + # statements from all transactions, 0.0 never logs + +#log_startup_progress_interval = 10s # Time between progress updates for + # long-running startup operations. + # 0 disables the feature, > 0 indicates + # the interval in milliseconds. + +# - What to Log - + +#debug_print_parse = off +#debug_print_rewritten = off +#debug_print_plan = off +#debug_pretty_print = on +#log_autovacuum_min_duration = 10min # log autovacuum activity; + # -1 disables, 0 logs all actions and + # their durations, > 0 logs only + # actions running at least this number + # of milliseconds. +#log_checkpoints = on +#log_connections = off +#log_disconnections = off +#log_duration = off +#log_error_verbosity = default # terse, default, or verbose messages +#log_hostname = off +#log_line_prefix = '%m [%p] ' # special values: + # %a = application name + # %u = user name + # %d = database name + # %r = remote host and port + # %h = remote host + # %b = backend type + # %p = process ID + # %P = process ID of parallel group leader + # %t = timestamp without milliseconds + # %m = timestamp with milliseconds + # %n = timestamp with milliseconds (as a Unix epoch) + # %Q = query ID (0 if none or not computed) + # %i = command tag + # %e = SQL state + # %c = session ID + # %l = session line number + # %s = session start timestamp + # %v = virtual transaction ID + # %x = transaction ID (0 if none) + # %q = stop here in non-session + # processes + # %% = '%' + # e.g. '<%u%%%d> ' +#log_lock_waits = off # log lock waits >= deadlock_timeout +#log_recovery_conflict_waits = off # log standby recovery conflict waits + # >= deadlock_timeout +#log_parameter_max_length = -1 # when logging statements, limit logged + # bind-parameter values to N bytes; + # -1 means print in full, 0 disables +#log_parameter_max_length_on_error = 0 # when logging an error, limit logged + # bind-parameter values to N bytes; + # -1 means print in full, 0 disables +#log_statement = 'none' # none, ddl, mod, all +#log_replication_commands = off +#log_temp_files = -1 # log temporary files equal or larger + # than the specified size in kilobytes; + # -1 disables, 0 logs all temp files +log_timezone = 'Etc/UTC' + +# - Process Title - + +#cluster_name = '' # added to process titles if nonempty + # (change requires restart) +#update_process_title = on + + +#------------------------------------------------------------------------------ +# STATISTICS +#------------------------------------------------------------------------------ + +# - Cumulative Query and Index Statistics - + +#track_activities = on +#track_activity_query_size = 1024 # (change requires restart) +#track_counts = on +#track_io_timing = off +#track_wal_io_timing = off +#track_functions = none # none, pl, all +#stats_fetch_consistency = cache # cache, none, snapshot + + +# - Monitoring - + +#compute_query_id = auto +#log_statement_stats = off +#log_parser_stats = off +#log_planner_stats = off +#log_executor_stats = off + + +#------------------------------------------------------------------------------ +# AUTOVACUUM +#------------------------------------------------------------------------------ + +#autovacuum = on # Enable autovacuum subprocess? 'on' + # requires track_counts to also be on. +#autovacuum_max_workers = 3 # max number of autovacuum subprocesses + # (change requires restart) +#autovacuum_naptime = 1min # time between autovacuum runs +#autovacuum_vacuum_threshold = 50 # min number of row updates before + # vacuum +#autovacuum_vacuum_insert_threshold = 1000 # min number of row inserts + # before vacuum; -1 disables insert + # vacuums +#autovacuum_analyze_threshold = 50 # min number of row updates before + # analyze +#autovacuum_vacuum_scale_factor = 0.2 # fraction of table size before vacuum +#autovacuum_vacuum_insert_scale_factor = 0.2 # fraction of inserts over table + # size before insert vacuum +#autovacuum_analyze_scale_factor = 0.1 # fraction of table size before analyze +#autovacuum_freeze_max_age = 200000000 # maximum XID age before forced vacuum + # (change requires restart) +#autovacuum_multixact_freeze_max_age = 400000000 # maximum multixact age + # before forced vacuum + # (change requires restart) +#autovacuum_vacuum_cost_delay = 2ms # default vacuum cost delay for + # autovacuum, in milliseconds; + # -1 means use vacuum_cost_delay +#autovacuum_vacuum_cost_limit = -1 # default vacuum cost limit for + # autovacuum, -1 means use + # vacuum_cost_limit + + +#------------------------------------------------------------------------------ +# CLIENT CONNECTION DEFAULTS +#------------------------------------------------------------------------------ + +# - Statement Behavior - + +#client_min_messages = notice # values in order of decreasing detail: + # debug5 + # debug4 + # debug3 + # debug2 + # debug1 + # log + # notice + # warning + # error +#search_path = '"$user", public' # schema names +#row_security = on +#default_table_access_method = 'heap' +#default_tablespace = '' # a tablespace name, '' uses the default +#default_toast_compression = 'pglz' # 'pglz' or 'lz4' +#temp_tablespaces = '' # a list of tablespace names, '' uses + # only default tablespace +#check_function_bodies = on +#default_transaction_isolation = 'read committed' +#default_transaction_read_only = off +#default_transaction_deferrable = off +#session_replication_role = 'origin' +#statement_timeout = 0 # in milliseconds, 0 is disabled +#transaction_timeout = 0 # in milliseconds, 0 is disabled +#lock_timeout = 0 # in milliseconds, 0 is disabled +#idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled +#idle_session_timeout = 0 # in milliseconds, 0 is disabled +#vacuum_freeze_table_age = 150000000 +#vacuum_freeze_min_age = 50000000 +#vacuum_failsafe_age = 1600000000 +#vacuum_multixact_freeze_table_age = 150000000 +#vacuum_multixact_freeze_min_age = 5000000 +#vacuum_multixact_failsafe_age = 1600000000 +#bytea_output = 'hex' # hex, escape +#xmlbinary = 'base64' +#xmloption = 'content' +#gin_pending_list_limit = 4MB +#createrole_self_grant = '' # set and/or inherit +#event_triggers = on + +# - Locale and Formatting - + +datestyle = 'iso, mdy' +#intervalstyle = 'postgres' +timezone = 'Etc/UTC' +#timezone_abbreviations = 'Default' # Select the set of available time zone + # abbreviations. Currently, there are + # Default + # Australia (historical usage) + # India + # You can create your own file in + # share/timezonesets/. +#extra_float_digits = 1 # min -15, max 3; any value >0 actually + # selects precise output mode +#client_encoding = sql_ascii # actually, defaults to database + # encoding + +# These settings are initialized by initdb, but they can be changed. +lc_messages = 'en_US.utf8' # locale for system error message + # strings +lc_monetary = 'en_US.utf8' # locale for monetary formatting +lc_numeric = 'en_US.utf8' # locale for number formatting +lc_time = 'en_US.utf8' # locale for time formatting + +#icu_validation_level = warning # report ICU locale validation + # errors at the given level + +# default configuration for text search +default_text_search_config = 'pg_catalog.english' + +# - Shared Library Preloading - + +#local_preload_libraries = '' +#session_preload_libraries = '' +#shared_preload_libraries = '' # (change requires restart) +#jit_provider = 'llvmjit' # JIT library to use + +# - Other Defaults - + +#dynamic_library_path = '$libdir' +#extension_destdir = '' # prepend path when loading extensions + # and shared objects (added by Debian) +#gin_fuzzy_search_limit = 0 + + +#------------------------------------------------------------------------------ +# LOCK MANAGEMENT +#------------------------------------------------------------------------------ + +#deadlock_timeout = 1s +#max_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_transaction = 64 # min 10 + # (change requires restart) +#max_pred_locks_per_relation = -2 # negative values mean + # (max_pred_locks_per_transaction + # / -max_pred_locks_per_relation) - 1 +#max_pred_locks_per_page = 2 # min 0 + + +#------------------------------------------------------------------------------ +# VERSION AND PLATFORM COMPATIBILITY +#------------------------------------------------------------------------------ + +# - Previous PostgreSQL Versions - + +#array_nulls = on +#backslash_quote = safe_encoding # on, off, or safe_encoding +#escape_string_warning = on +#lo_compat_privileges = off +#quote_all_identifiers = off +#standard_conforming_strings = on +#synchronize_seqscans = on + +# - Other Platforms and Clients - + +#transform_null_equals = off +#allow_alter_system = on + + +#------------------------------------------------------------------------------ +# ERROR HANDLING +#------------------------------------------------------------------------------ + +#exit_on_error = off # terminate session on any error? +#restart_after_crash = on # reinitialize after backend crash? +#data_sync_retry = off # retry or panic on failure to fsync + # data? + # (change requires restart) +#recovery_init_sync_method = fsync # fsync, syncfs (Linux 5.8+) + + +#------------------------------------------------------------------------------ +# CONFIG FILE INCLUDES +#------------------------------------------------------------------------------ + +# These options allow settings to be loaded from files other than the +# default postgresql.conf. Note that these are directives, not variable +# assignments, so they can usefully be given more than once. + +#include_dir = '...' # include files ending in '.conf' from + # a directory, e.g., 'conf.d' +#include_if_exists = '...' # include file only if it exists +#include = '...' # include file + + +#------------------------------------------------------------------------------ +# CUSTOMIZED OPTIONS +#------------------------------------------------------------------------------ + +# Add settings for extensions here diff --git a/engine/test/1.synthetic.sh b/engine/test/1.synthetic.sh index 92d2f167..5470eb51 100644 --- a/engine/test/1.synthetic.sh +++ b/engine/test/1.synthetic.sh @@ -45,8 +45,6 @@ for i in {1..300}; do sleep 1 done -check_database_readiness || (echo "test database is not ready" && exit 1) - # Restart container explicitly after initdb to make sure that the server will not receive a shutdown request and queries will not be interrupted. sudo docker restart dblab_pg_initdb @@ -55,8 +53,6 @@ for i in {1..300}; do sleep 1 done -check_database_readiness || (echo "test database is not ready" && exit 1) - # Create the test database sudo docker exec dblab_pg_initdb psql -U postgres -c 'create database test' diff --git a/engine/test/2.logical_generic.sh b/engine/test/2.logical_generic.sh index 73b5f2aa..eb185c7a 100644 --- a/engine/test/2.logical_generic.sh +++ b/engine/test/2.logical_generic.sh @@ -51,8 +51,6 @@ if [[ "${SOURCE_HOST}" = "172.17.0.1" ]]; then sleep 1 done - check_database_readiness || (echo "test database is not ready" && exit 1) - check_data_existence(){ sudo docker exec postgres"${POSTGRES_VERSION}" psql -d "${SOURCE_DBNAME}" -U postgres --command 'select from pgbench_accounts' > /dev/null 2>&1 return $? diff --git a/engine/test/4.physical_basebackup.sh b/engine/test/4.physical_basebackup.sh index 2af38d5f..f72508b5 100644 --- a/engine/test/4.physical_basebackup.sh +++ b/engine/test/4.physical_basebackup.sh @@ -50,8 +50,6 @@ if [[ "${SOURCE_HOST}" = "172.17.0.1" ]]; then sleep 1 done - check_database_readiness || (echo "test database is not ready" && exit 1) - # add "host replication" to pg_hba.conf sudo docker exec postgres"${POSTGRES_VERSION}" bash -c 'echo "host replication all 0.0.0.0/0 md5" >> $PGDATA/pg_hba.conf' # reload conf diff --git a/engine/test/_cleanup.sh b/engine/test/_cleanup.sh index 2800fa76..6fb304a7 100644 --- a/engine/test/_cleanup.sh +++ b/engine/test/_cleanup.sh @@ -6,14 +6,18 @@ DLE_TEST_POOL_NAME="test_dblab_pool" ZFS_FILE="$(pwd)/zfs_file" # Stop and remove test Docker containers -sudo docker ps -aq --filter label="test_dblab_pool" | xargs --no-run-if-empty sudo docker rm -f -sudo docker ps -aq --filter label="dblab_test" | xargs --no-run-if-empty sudo docker rm -f +sudo docker ps -aq --filter label="test_dblab_pool" | xargs --no-run-if-empty sudo docker rm -f \ + || echo "Failed to remove test Docker containers, continuing..." +sudo docker ps -aq --filter label="dblab_test" | xargs --no-run-if-empty sudo docker rm -f \ + || echo "Failed to remove dblab_test Docker containers, continuing..." # Remove unused Docker images -sudo docker images --filter=reference='registry.gitlab.com/postgres-ai/database-lab/dblab-server:*' -q | xargs --no-run-if-empty sudo docker rmi || echo "Docker image removal finished with errors but it is OK to ignore them." +sudo docker images --filter=reference='registry.gitlab.com/postgres-ai/database-lab/dblab-server:*' -q | xargs --no-run-if-empty sudo docker rmi \ + || echo "Docker image removal finished with errors but it is OK to ignore them." # Clean up data directory -sudo rm -rf ${DLE_TEST_MOUNT_DIR}/${DLE_TEST_POOL_NAME}/data/* +sudo rm -rf ${DLE_TEST_MOUNT_DIR}/${DLE_TEST_POOL_NAME}/data/* \ + || echo "Data directory cleanup finished with errors but continuing..." # Remove dump directory sudo umount ${DLE_TEST_MOUNT_DIR}/${DLE_TEST_POOL_NAME}/dump \ @@ -30,7 +34,9 @@ sudo zpool destroy test_dblab_pool \ || echo "Destroying ZFS storage pool finished with errors but it is OK to ignore them." # Remove ZFS FILE -sudo rm -f "${ZFS_FILE}" +sudo rm -f "${ZFS_FILE}" \ + || echo "Failed to remove ZFS file, but continuing..." # Remove CLI configuration -dblab config remove test || true +dblab config remove test \ + || echo "Removing CLI configuration finished with errors but it is OK to ignore them." diff --git a/translations/README.german.md b/translations/README.german.md index 7e805df7..4900b14c 100644 --- a/translations/README.german.md +++ b/translations/README.german.md @@ -80,7 +80,7 @@ Weiterlesen: - Blitzschnelles Klonen von Postgres-Datenbanken. Es wird ein paar Sekunden gebraucht, um einen neuen Klon zu erstellen, der bereit ist, Verbindungen und Abfragen zu akzeptieren, unabhängig von der Datenbankgröße. - Die theoretische maximale Anzahl von Snapshots und Klonen beträgt 264 ([ZFS](https://en.wikipedia.org/wiki/ZFS), Standard). - Theoretische maximale Größe des PostgreSQL-Datenverzeichnisses: 256 Billiarden Zebibyte oder 2128 Byte ([ZFS](https://en.wikipedia.org/wiki/ZFS), Standard). -- Unterstützte Hauptversionen von PostgreSQL: 9.6–16. +- Unterstützte Hauptversionen von PostgreSQL: 9.6–17. - Zwei Technologien werden unterstützt, um Thin Cloning zu ermöglichen ([CoW](https://en.wikipedia.org/wiki/Copy-on-write)): [ZFS](https://en.wikipedia.org/wiki/ ZFS) und [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)). - Alle Komponenten sind in Docker-Containern verpackt. - UI macht die manuelle Arbeit bequemer. diff --git a/translations/README.portuguese-br.md b/translations/README.portuguese-br.md index 1ab510c9..1ce67592 100644 --- a/translations/README.portuguese-br.md +++ b/translations/README.portuguese-br.md @@ -80,7 +80,7 @@ Leia mais: - Clonagem the bancos de dados Postgres ultrarrápidos - apenas alguns segundos para criar um novo clone pronto para aceitar conexões e queries, independentemente do tamanho do banco de dados. - O número máximo teórico de snapshots e clones é 264 ([ZFS](https://en.wikipedia.org/wiki/ZFS), default). - O número máximo teórico de do diretório de dados do PostgreSQL: 256 quatrilhões zebibytes, ou 2128 bytes ([ZFS](https://en.wikipedia.org/wiki/ZFS), default). -- Versões _major_ do PostgreSQL suportadas: 9.6–16. +- Versões _major_ do PostgreSQL suportadas: 9.6–17. - Duas tecnologias são suportadas para viabilizar o thin cloning ([CoW](https://en.wikipedia.org/wiki/Copy-on-write)): [ZFS](https://en.wikipedia.org/wiki/ZFS) e [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)). - Todos os componentes estão empacotados em docker containers. - UI para tornar o trabalho manual mais conveniente. diff --git a/translations/README.russian.md b/translations/README.russian.md index ad2e4733..8a4925d8 100644 --- a/translations/README.russian.md +++ b/translations/README.russian.md @@ -81,7 +81,7 @@ - Молниеносное клонирование БД Postgres - создание нового клона, готового к работе, всего за несколько секунд (вне зависимости от размера БД). - Максимальное теоретическое количество снимков: 264. ([ZFS](https://en.wikipedia.org/wiki/ZFS), вариант по умолчанию). - Максимальный теоретический размер директории данных PostgreSQL: 256 квадриллионов зебибайт или 2128 байт ([ZFS](https://en.wikipedia.org/wiki/ZFS), вариант по умолчанию). -- Поддерживаются все основные версии PostgreSQL: 9.6-16. +- Поддерживаются все основные версии PostgreSQL: 9.6-17. - Для реализации тонкого клонирования поддерживаются две технологии ([CoW](https://en.wikipedia.org/wiki/Copy-on-write)): [ZFS](https://en.wikipedia.org/wiki/ZFS) и [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)). - Все компоненты работают в Docker-контейнерах. - UI для удобства ручных действий пользователя. diff --git a/translations/README.spanish.md b/translations/README.spanish.md index 10719d0f..903dca3e 100644 --- a/translations/README.spanish.md +++ b/translations/README.spanish.md @@ -80,7 +80,7 @@ Lee más: - Clonación ultrarrápida de bases de datos de Postgres: unos segundos para crear un nuevo clon listo para aceptar conexiones y consultas, independientemente del tamaño de la base de datos. - El número máximo teórico de instantáneas y clones es 264 ([ZFS](https://en.wikipedia.org/wiki/ZFS), predeterminado). - El tamaño máximo teórico del directorio de datos de PostgreSQL: 256 cuatrillones de zebibytes, o 2128 bytes ([ZFS](https://en.wikipedia.org/wiki/ZFS), predeterminado). -- Versiones principales de PostgreSQL admitidas: 9.6–16. +- Versiones principales de PostgreSQL admitidas: 9.6–17. - Se admiten dos tecnologías para permitir la clonación ligera ([CoW](https://en.wikipedia.org/wiki/Copy-on-write)): [ZFS](https://en.wikipedia.org/wiki/ZFS) y [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)). - Todos los componentes están empaquetados en contenedores Docker. - Interfaz de usuario para que el trabajo manual sea más conveniente. diff --git a/translations/README.ukrainian.md b/translations/README.ukrainian.md index e3d7dbf1..402fec8e 100644 --- a/translations/README.ukrainian.md +++ b/translations/README.ukrainian.md @@ -81,7 +81,7 @@ - блискавичне клонування БД Postgres - створення нового клону, готового до роботи, всього за кілька секунд (незалежно від розміру БД). - Максимальна теоретична кількість знімків: 264. ([ZFS](https://en.wikipedia.org/wiki/ZFS), варіант за замовчуванням). - Максимальний теоретичний розмір директорії даних PostgreSQL: 256 квадрильйонів зебібайт або 2128 байт ([ZFS](https://en.wikipedia.org/wiki/ZFS), варіант за замовчуванням). -- Підтримуються усі основні версії PostgreSQL: 9.6-16. +- Підтримуються усі основні версії PostgreSQL: 9.6-17. - Для реалізації тонкого клонування підтримуються дві технології ([CoW](https://en.wikipedia.org/wiki/Copy-on-write)): [ZFS](https://en.wikipedia.org/wiki/ZFS ) та [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)). - Усі компоненти працюють у Docker-контейнерах. - UI для зручності ручних дій користувача. diff --git a/ui/packages/shared/pages/Configuration/utils/index.ts b/ui/packages/shared/pages/Configuration/utils/index.ts index 27c732fd..216de9c9 100644 --- a/ui/packages/shared/pages/Configuration/utils/index.ts +++ b/ui/packages/shared/pages/Configuration/utils/index.ts @@ -7,14 +7,15 @@ const seContainerRegistry = 'se-images' const genericImagePrefix = 'postgresai/extended-postgres' // since some tags are rc, we need to specify the exact tags to use const dockerImagesConfig = { - '9.6': ['0.4.6', '0.4.5', '0.4.4'], - '10': ['0.4.6', '0.4.5', '0.4.4'], - '11': ['0.4.6', '0.4.5', '0.4.4'], - '12': ['0.4.6', '0.4.5', '0.4.4'], - '13': ['0.4.6', '0.4.5', '0.4.4'], - '14': ['0.4.6', '0.4.5', '0.4.4'], - '15': ['0.4.6', '0.4.5', '0.4.4'], - '16': ['0.4.6', '0.4.5', '0.4.4'], + '9.6': ['0.5.0', '0.4.6', '0.4.5'], + '10': ['0.5.0', '0.4.6', '0.4.5'], + '11': ['0.5.0', '0.4.6', '0.4.5'], + '12': ['0.5.0', '0.4.6', '0.4.5'], + '13': ['0.5.0', '0.4.6', '0.4.5'], + '14': ['0.5.0', '0.4.6', '0.4.5'], + '15': ['0.5.0', '0.4.6', '0.4.5'], + '16': ['0.5.0', '0.4.6', '0.4.5'], + '17rc1': ['0.5.0'], } export type FormValuesKey = keyof FormValues From 5e7b3eaa67e64d4c4d7deea412a0bb5dea6685d2 Mon Sep 17 00:00:00 2001 From: Nikolay Samokhvalov Date: Fri, 20 Sep 2024 19:07:01 +0000 Subject: [PATCH 066/111] PG16 is default; a note about matching version --- engine/configs/config.example.logical_generic.yml | 11 +++++++---- engine/configs/config.example.logical_rds_iam.yml | 11 +++++++---- engine/configs/config.example.physical_generic.yml | 11 +++++++---- engine/configs/config.example.physical_pgbackrest.yml | 11 +++++++---- engine/configs/config.example.physical_walg.yml | 11 +++++++---- 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/engine/configs/config.example.logical_generic.yml b/engine/configs/config.example.logical_generic.yml index 7a85bfa2..e08d1c28 100644 --- a/engine/configs/config.example.logical_generic.yml +++ b/engine/configs/config.example.logical_generic.yml @@ -98,10 +98,13 @@ databaseContainer: &db_container # We need to specify which Postgres Docker image is to be used for that. # The default is the extended Postgres image built on top of the official Postgres image # (See https://postgres.ai/docs/database-lab/supported_databases). - # It is possible to choose any custom or official Docker image that runs Postgres. Our Dockerfile - # (See https://gitlab.com/postgres-ai/custom-images/-/tree/master/extended) - # is recommended in case if customization is needed. - dockerImage: "postgresai/extended-postgres:16-0.4.6" + # + # For paid customers, Postgres.AI maintains a set of images compatible with + # RDS and RDS Aurora, GCP CloudSQL, Heroku, Timescale Cloud, Supabase, PostGIS. + # + # IMPORTANT: Postgres version of this image should match the source's Postgres version. + # For logical mode, it is a recommendation. For physical mode, it is a *requirement*. + dockerImage: "postgresai/extended-postgres:16-0.5.0" # Container parameters, see # https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources diff --git a/engine/configs/config.example.logical_rds_iam.yml b/engine/configs/config.example.logical_rds_iam.yml index 444d246f..507779e4 100644 --- a/engine/configs/config.example.logical_rds_iam.yml +++ b/engine/configs/config.example.logical_rds_iam.yml @@ -97,10 +97,13 @@ databaseContainer: &db_container # We need to specify which Postgres Docker image is to be used for that. # The default is the extended Postgres image built on top of the official Postgres image # (See https://postgres.ai/docs/database-lab/supported_databases). - # It is possible to choose any custom or official Docker image that runs Postgres. Our Dockerfile - # (See https://gitlab.com/postgres-ai/custom-images/-/tree/master/extended) - # is recommended in case if customization is needed. - dockerImage: "postgresai/extended-postgres:16-0.4.6" + # + # For paid customers, Postgres.AI maintains a set of images compatible with + # RDS and RDS Aurora, GCP CloudSQL, Heroku, Timescale Cloud, Supabase, PostGIS. + # + # IMPORTANT: Postgres version of this image should match the source's Postgres version. + # For logical mode, it is a recommendation. For physical mode, it is a *requirement*. + dockerImage: "postgresai/extended-postgres:16-0.5.0" # Custom parameters for containers with PostgreSQL, see # https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources diff --git a/engine/configs/config.example.physical_generic.yml b/engine/configs/config.example.physical_generic.yml index 858c66bd..ebf29392 100644 --- a/engine/configs/config.example.physical_generic.yml +++ b/engine/configs/config.example.physical_generic.yml @@ -97,10 +97,13 @@ databaseContainer: &db_container # We need to specify which Postgres Docker image is to be used for that. # The default is the extended Postgres image built on top of the official Postgres image # (See https://postgres.ai/docs/database-lab/supported_databases). - # Any custom or official Docker image that runs Postgres. Our Dockerfile - # (See https://gitlab.com/postgres-ai/custom-images/-/tree/master/extended) - # is recommended in case if customization is needed. - dockerImage: "postgresai/extended-postgres:16-0.4.6" + # + # For paid customers, Postgres.AI maintains a set of images compatible with + # RDS and RDS Aurora, GCP CloudSQL, Heroku, Timescale Cloud, Supabase, PostGIS. + # + # IMPORTANT: Postgres version of this image should match the source's Postgres version. + # For logical mode, it is a recommendation. For physical mode, it is a *requirement*. + dockerImage: "postgresai/extended-postgres:16-0.5.0" # Custom parameters for containers with PostgreSQL, see # https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources diff --git a/engine/configs/config.example.physical_pgbackrest.yml b/engine/configs/config.example.physical_pgbackrest.yml index 04d9ed8e..447272e9 100644 --- a/engine/configs/config.example.physical_pgbackrest.yml +++ b/engine/configs/config.example.physical_pgbackrest.yml @@ -97,10 +97,13 @@ databaseContainer: &db_container # We need to specify which Postgres Docker image is to be used for that. # The default is the extended Postgres image built on top of the official Postgres image # (See https://postgres.ai/docs/database-lab/supported_databases). - # Any custom or official Docker image that runs Postgres. Our Dockerfile - # (See https://gitlab.com/postgres-ai/custom-images/-/tree/master/extended) - # is recommended in case if customization is needed. - dockerImage: "postgresai/extended-postgres:16-0.4.6" + # + # For paid customers, Postgres.AI additionally maintains a set of images compatible with + # RDS and RDS Aurora, GCP CloudSQL, Heroku, Timescale Cloud, Supabase, PostGIS. + # + # IMPORTANT: Postgres version of this image should match the source's Postgres version. + # For logical mode, it is a recommendation. For physical mode, it is a *requirement*. + dockerImage: "postgresai/extended-postgres:16-0.5.0" # Custom parameters for containers with PostgreSQL, see # https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources diff --git a/engine/configs/config.example.physical_walg.yml b/engine/configs/config.example.physical_walg.yml index 66060f7c..17a4167c 100644 --- a/engine/configs/config.example.physical_walg.yml +++ b/engine/configs/config.example.physical_walg.yml @@ -97,10 +97,13 @@ databaseContainer: &db_container # We need to specify which Postgres Docker image is to be used for that. # The default is the extended Postgres image built on top of the official Postgres image # (See https://postgres.ai/docs/database-lab/supported_databases). - # Any custom or official Docker image that runs Postgres. Our Dockerfile - # (See https://gitlab.com/postgres-ai/custom-images/-/tree/master/extended) - # is recommended in case if customization is needed. - dockerImage: "postgresai/extended-postgres:16-0.4.6" + # + # For paid customers, Postgres.AI additionally maintains a set of images compatible with + # RDS and RDS Aurora, GCP CloudSQL, Heroku, Timescale Cloud, Supabase, PostGIS. + # + # IMPORTANT: Postgres version of this image should match the source's Postgres version. + # For logical mode, it is a recommendation. For physical mode, it is a *requirement*. + dockerImage: "postgresai/extended-postgres:16-0.5.0" # Custom parameters for containers with PostgreSQL, see # https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources From f0112c3875d57242aecbbc7b805258bab6fdb28e Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 26 Sep 2024 01:01:53 +0000 Subject: [PATCH 067/111] Bot UI: Hide permalink in private thread --- .../platform/src/pages/Bot/Messages/Message/Message.tsx | 8 +++++--- ui/packages/platform/src/pages/Bot/Messages/Messages.tsx | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx index 2cb45d68..40d63dc1 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx @@ -23,6 +23,7 @@ type BaseMessageProps = { aiModel?: string stateMessage?: StateMessage | null isCurrentStreamMessage?: boolean + isPublic?: boolean; } type AiMessageProps = BaseMessageProps & { @@ -35,7 +36,7 @@ type AiMessageProps = BaseMessageProps & { type HumanMessageProps = BaseMessageProps & { isAi: false; name: string; - content: string + content: string; } type LoadingMessageProps = BaseMessageProps & { @@ -259,7 +260,8 @@ export const Message = React.memo((props: MessageProps) => { isLoading, aiModel, stateMessage, - isCurrentStreamMessage + isCurrentStreamMessage, + isPublic } = props; const [isDebugVisible, setDebugVisible] = useState(false); @@ -322,7 +324,7 @@ export const Message = React.memo((props: MessageProps) => { {formattedTime} }
- {id && <> + {id && isPublic && <> | { slack_profile, created_at, content, - ai_model + ai_model, + is_public } = message; let name = 'You'; @@ -274,6 +275,7 @@ export const Messages = React.memo(({orgId}: {orgId: number}) => { content={content} formattedTime={formattedTime} aiModel={ai_model} + isPublic={is_public} /> ) })} From 1e60b62ad71ba69b3908b42c642d95518c1e8ae3 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 26 Sep 2024 01:02:14 +0000 Subject: [PATCH 068/111] Fixed line breaks in the menu --- .../platform/src/components/IndexPage/IndexPageWrapper.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx b/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx index 82a2742c..156abe51 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx @@ -224,7 +224,8 @@ export const IndexPageWrapper = (props: IndexPageProps) => { paddingLeft: '15px', color: '#000000', display: 'inline-flex', - alignItems: 'center' + alignItems: 'center', + whiteSpace: 'nowrap' }, menuSectionHeaderActiveLink: { textDecoration: 'none', From a3ff7d61066e72890a10af3d0ee57ee034c37a8d Mon Sep 17 00:00:00 2001 From: Artyom Kartasov Date: Thu, 26 Sep 2024 04:59:57 +0000 Subject: [PATCH 069/111] chore: update Go, golangci-lint and deps --- engine/.gitlab-ci.yml | 4 +- engine/.golangci.yml | 33 ++++---- engine/Dockerfile.dblab-server-debug | 2 +- engine/Makefile | 2 +- engine/cmd/cli/main.go | 2 +- engine/go.mod | 30 +++++-- engine/go.sum | 83 +++++++++++++++---- engine/internal/embeddedui/embedded_ui.go | 3 +- engine/internal/provision/mode_local.go | 5 +- .../retrieval/engine/postgres/logical/dump.go | 2 +- .../engine/postgres/logical/restore.go | 2 +- .../engine/postgres/physical/physical.go | 4 +- .../engine/postgres/snapshot/logical.go | 3 +- .../engine/postgres/snapshot/physical.go | 2 +- .../engine/postgres/tools/cont/container.go | 6 +- .../engine/postgres/tools/db/image_content.go | 2 +- .../retrieval/engine/postgres/tools/tools.go | 8 +- engine/internal/runci/handlers.go | 2 +- engine/internal/srv/ws.go | 4 +- engine/pkg/util/projection/operations.go | 7 ++ 20 files changed, 141 insertions(+), 65 deletions(-) diff --git a/engine/.gitlab-ci.yml b/engine/.gitlab-ci.yml index 94cc6271..47ec2276 100644 --- a/engine/.gitlab-ci.yml +++ b/engine/.gitlab-ci.yml @@ -1,5 +1,5 @@ default: - image: golang:1.20 + image: golang:1.23 stages: - test @@ -56,7 +56,7 @@ lint: ### Build binary. build-binary-alpine: <<: *only_engine - image: golang:1.20-alpine + image: golang:1.23-alpine stage: build-binary artifacts: paths: diff --git a/engine/.golangci.yml b/engine/.golangci.yml index 1e9892c4..c8a38ec1 100644 --- a/engine/.golangci.yml +++ b/engine/.golangci.yml @@ -2,10 +2,9 @@ run: timeout: 2m issues-exit-code: 1 tests: true - skip-dirs: - - vendor output: - format: colored-line-number + formats: + - format: colored-line-number print-issued-lines: true print-linter-name: true @@ -22,10 +21,8 @@ linters-settings: gofmt: simplify: true gofumpt: - lang-version: "1.17" extra-rules: false gosimple: - go: "1.18" checks: [ "all" ] goimports: local-prefixes: gitlab.com/postgres-ai/database-lab @@ -37,14 +34,17 @@ linters-settings: lll: line-length: 140 tab-width: 1 - gomnd: - settings: - mnd: - ignored-functions: strconv.Format*,os.*,strconv.Parse*,strings.SplitN,bytes.SplitN + mnd: + ignored-functions: + - strconv.Format* + - os.* + - strconv.Parse* + - strings.SplitN + - bytes.SplitN revive: - min-confidence: 0.8 + confidence: 0.8 unused: - check-exported: false + exported-fields-are-used: false unparam: check-exported: false nakedret: @@ -72,15 +72,15 @@ linters: - goconst - gocritic - goimports - - gomnd - gosimple - govet - ineffassign - lll - - megacheck - misspell + - mnd - prealloc - revive + - staticcheck - stylecheck - unconvert - unused @@ -90,9 +90,8 @@ linters: disable: - depguard - gosec - - interfacer - gocyclo # currently unmaintained - presets: + #presets: fast: false issues: @@ -104,7 +103,9 @@ issues: - lll - errcheck - wsl - - gomnd + - mnd + exclude-dirs: + - vendor exclude-use-default: false max-issues-per-linter: 0 diff --git a/engine/Dockerfile.dblab-server-debug b/engine/Dockerfile.dblab-server-debug index 35181e62..af6b1f17 100644 --- a/engine/Dockerfile.dblab-server-debug +++ b/engine/Dockerfile.dblab-server-debug @@ -1,7 +1,7 @@ # How to start a container: https://postgres.ai/docs/how-to-guides/administration/engine-manage # Compile stage -FROM golang:1.18 AS build-env +FROM golang:1.23 AS build-env # Build Delve RUN go install github.com/go-delve/delve/cmd/dlv@latest diff --git a/engine/Makefile b/engine/Makefile index 50143634..84bf96de 100644 --- a/engine/Makefile +++ b/engine/Makefile @@ -34,7 +34,7 @@ help: ## Display the help message all: clean build ## Build all binary components of the project install-lint: ## Install the linter to $GOPATH/bin which is expected to be in $PATH - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.3 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.61.0 run-lint: ## Run linters golangci-lint run diff --git a/engine/cmd/cli/main.go b/engine/cmd/cli/main.go index 205e10ab..5bd6c4ed 100644 --- a/engine/cmd/cli/main.go +++ b/engine/cmd/cli/main.go @@ -24,7 +24,7 @@ func main() { app := &cli.App{ Version: version.GetVersion(), CommandNotFound: func(c *cli.Context, command string) { - fmt.Fprintf(c.App.Writer, "[ERROR] Command %q not found.\n", command) + _, _ = fmt.Fprintf(c.App.Writer, "[ERROR] Command %q not found.\n", command) }, Before: loadEnvironmentParams, Commands: joinCommands( diff --git a/engine/go.mod b/engine/go.mod index d398ff5a..ec0ddf86 100644 --- a/engine/go.mod +++ b/engine/go.mod @@ -1,14 +1,14 @@ module gitlab.com/postgres-ai/database-lab/v3 -go 1.20 +go 1.23 require ( github.com/AlekSi/pointer v1.2.0 github.com/ahmetalpbalkan/dlog v0.0.0-20170105205344-4fb5f8204f26 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/aws/aws-sdk-go v1.44.309 - github.com/docker/cli v24.0.9+incompatible - github.com/docker/docker v24.0.9+incompatible + github.com/docker/cli v25.0.6+incompatible + github.com/docker/docker v25.0.6+incompatible github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.5.0 github.com/dustin/go-humanize v1.0.1 @@ -27,7 +27,7 @@ require ( github.com/sergi/go-diff v1.3.1 github.com/sethvargo/go-password v0.2.0 github.com/shirou/gopsutil v3.21.11+incompatible - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.12.0 github.com/urfave/cli/v2 v2.25.7 github.com/wagslane/go-password-validator v0.3.0 @@ -43,9 +43,14 @@ require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/containerd/containerd v1.7.2 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -58,26 +63,37 @@ require ( github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.16.7 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/magiconair/properties v1.8.5 // indirect github.com/moby/patternmatcher v0.5.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.3.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc4 // indirect - github.com/opencontainers/runc v1.1.12 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect + go.opentelemetry.io/otel v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect + go.opentelemetry.io/otel/sdk v1.18.0 // indirect + go.opentelemetry.io/otel/trace v1.30.0 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.11.0 // indirect google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/engine/go.sum b/engine/go.sum index b60e040a..9be68150 100644 --- a/engine/go.sum +++ b/engine/go.sum @@ -23,6 +23,7 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -60,6 +61,7 @@ github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2 github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= github.com/Microsoft/hcsshim v0.10.0-rc.8 h1:YSZVvlIIDD1UxQpJp0h+dnpLUw+TrY0cx8obKsp3bek= +github.com/Microsoft/hcsshim v0.10.0-rc.8/go.mod h1:OEthFdQv/AD2RAdzR6Mm1N1KPCztGKDurW1Z8b8VGMM= github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -96,6 +98,8 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= @@ -160,6 +164,8 @@ github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0Z github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= @@ -196,10 +202,13 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= @@ -210,17 +219,17 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/cli v24.0.9+incompatible h1:OxbimnP/z+qVjDLpq9wbeFU3Nc30XhSe+LkwYQisD50= -github.com/docker/cli v24.0.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v25.0.6+incompatible h1:F1mCw1kUGixOkM8WQbcG5kniPvP8XCFxreFxl4b/UnY= +github.com/docker/cli v25.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= -github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v25.0.6+incompatible h1:5cPwbwriIcsua2REJe8HqQV+6WlWc1byg2QSXzBxBGg= +github.com/docker/docker v25.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= @@ -246,6 +255,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -266,6 +277,11 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= @@ -337,7 +353,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v34 v34.0.0 h1:/siYFImY8KwGc5QD1gaPf+f8QX6tLwxNIco2RkYxoFA= github.com/google/go-github/v34 v34.0.0/go.mod h1:w/2qlrXUfty+lbyO6tatnzIw97v1CM+/jZcwXMDiPQQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -372,7 +389,10 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= @@ -463,13 +483,15 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -507,6 +529,8 @@ github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdx github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= @@ -551,8 +575,6 @@ github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= -github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= -github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= @@ -566,6 +588,7 @@ github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xA github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -603,6 +626,9 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -653,8 +679,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -665,8 +692,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -717,6 +744,22 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0 h1:IAtl+7gua134xcV3NieDhJHjjOVeJhXAnYf/0hswjUY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.18.0/go.mod h1:w+pXobnBzh95MNIkeIuAKcHe/Uu/CX2PKIvBP6ipKRA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0 h1:6pu8ttx76BxHf+xz/H77AUZkPF3cwWzXqAUsXhVKI18= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.18.0/go.mod h1:IOmXxPrxoxFMXdNy7lfDmE8MzE61YPcurbUm0SMjerI= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/sdk v1.18.0 h1:e3bAB0wB3MljH38sHzpV/qWrOTCFrdZF2ct9F8rBkcY= +go.opentelemetry.io/otel/sdk v1.18.0/go.mod h1:1RCygWV7plY2KmdskZEDDBs4tJeHG92MdHZIluiYs/M= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -839,6 +882,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -921,6 +965,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -939,6 +984,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1031,6 +1077,10 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1045,6 +1095,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o= +google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1065,8 +1117,9 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/engine/internal/embeddedui/embedded_ui.go b/engine/internal/embeddedui/embedded_ui.go index 2fae98ff..d678aab1 100644 --- a/engine/internal/embeddedui/embedded_ui.go +++ b/engine/internal/embeddedui/embedded_ui.go @@ -13,7 +13,6 @@ import ( "strconv" "time" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" @@ -133,7 +132,7 @@ func (ui *UIManager) Run(ctx context.Context) error { return fmt.Errorf("failed to connect UI container to the internal Docker network: %w", err) } - if err := ui.docker.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil { + if err := ui.docker.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { return fmt.Errorf("failed to start container %q: %w", containerID, err) } diff --git a/engine/internal/provision/mode_local.go b/engine/internal/provision/mode_local.go index 54f4b3d8..9bbb9243 100644 --- a/engine/internal/provision/mode_local.go +++ b/engine/internal/provision/mode_local.go @@ -20,7 +20,7 @@ import ( "sync/atomic" "time" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/pkg/errors" @@ -514,6 +514,7 @@ func (p *Provisioner) allocatePort() (uint, error) { if err := p.portChecker.checkPortAvailability(host, port); err != nil { log.Msg(fmt.Sprintf("port %d is not available: %v", port, err)) + attempts++ continue @@ -803,7 +804,7 @@ func (p *Provisioner) ReconnectClone(ctx context.Context, cloneName string) erro // StartCloneContainer starts clone container. func (p *Provisioner) StartCloneContainer(ctx context.Context, containerName string) error { - return p.dockerClient.ContainerStart(ctx, containerName, types.ContainerStartOptions{}) + return p.dockerClient.ContainerStart(ctx, containerName, container.StartOptions{}) } // DetectDBVersion detects version of the database. diff --git a/engine/internal/retrieval/engine/postgres/logical/dump.go b/engine/internal/retrieval/engine/postgres/logical/dump.go index 4250363d..5f36b9f5 100644 --- a/engine/internal/retrieval/engine/postgres/logical/dump.go +++ b/engine/internal/retrieval/engine/postgres/logical/dump.go @@ -321,7 +321,7 @@ func (d *DumpJob) Run(ctx context.Context) (err error) { log.Msg(fmt.Sprintf("Running container: %s. ID: %v", d.dumpContainerName(), containerID)) - if err := d.dockerClient.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil { + if err := d.dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { collectDiagnostics(ctx, d.dockerClient, d.dumpContainerName(), dataDir) return errors.Wrapf(err, "failed to start container %q", d.dumpContainerName()) } diff --git a/engine/internal/retrieval/engine/postgres/logical/restore.go b/engine/internal/retrieval/engine/postgres/logical/restore.go index a56dc5ce..43b24440 100644 --- a/engine/internal/retrieval/engine/postgres/logical/restore.go +++ b/engine/internal/retrieval/engine/postgres/logical/restore.go @@ -245,7 +245,7 @@ func (r *RestoreJob) Run(ctx context.Context) (err error) { log.Msg(fmt.Sprintf("Running container: %s. ID: %v", r.restoreContainerName(), containerID)) - if err := r.dockerClient.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil { + if err := r.dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { return errors.Wrapf(err, "failed to start container %q", r.restoreContainerName()) } diff --git a/engine/internal/retrieval/engine/postgres/physical/physical.go b/engine/internal/retrieval/engine/postgres/physical/physical.go index 4d95ab46..2bfe97e4 100644 --- a/engine/internal/retrieval/engine/postgres/physical/physical.go +++ b/engine/internal/retrieval/engine/postgres/physical/physical.go @@ -229,7 +229,7 @@ func (r *RestoreJob) Run(ctx context.Context) (err error) { log.Msg(fmt.Sprintf("Running container: %s. ID: %v", r.restoreContainerName(), contID)) - if err = r.dockerClient.ContainerStart(ctx, contID, types.ContainerStartOptions{}); err != nil { + if err = r.dockerClient.ContainerStart(ctx, contID, container.StartOptions{}); err != nil { return errors.Wrapf(err, "failed to start container: %v", contID) } @@ -307,7 +307,7 @@ func (r *RestoreJob) startContainer(ctx context.Context, containerName string, c return "", fmt.Errorf("failed to create container %q %w", containerName, err) } - if err = r.dockerClient.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil { + if err = r.dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { return "", errors.Wrapf(err, "failed to start container %s", containerName) } diff --git a/engine/internal/retrieval/engine/postgres/snapshot/logical.go b/engine/internal/retrieval/engine/postgres/snapshot/logical.go index 1be78d7e..c596d6d5 100644 --- a/engine/internal/retrieval/engine/postgres/snapshot/logical.go +++ b/engine/internal/retrieval/engine/postgres/snapshot/logical.go @@ -11,7 +11,6 @@ import ( "path" "time" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" @@ -248,7 +247,7 @@ func (s *LogicalInitial) runPreprocessingQueries(ctx context.Context, dataDir st log.Msg(fmt.Sprintf("Running container: %s. ID: %v", s.patchContainerName(), containerID)) - if err := s.dockerClient.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil { + if err := s.dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { return errors.Wrap(err, "failed to start container") } diff --git a/engine/internal/retrieval/engine/postgres/snapshot/physical.go b/engine/internal/retrieval/engine/postgres/snapshot/physical.go index c13fb9fc..8bbd047c 100644 --- a/engine/internal/retrieval/engine/postgres/snapshot/physical.go +++ b/engine/internal/retrieval/engine/postgres/snapshot/physical.go @@ -628,7 +628,7 @@ func (p *PhysicalInitial) promoteInstance(ctx context.Context, clonePath string, log.Msg(fmt.Sprintf("Running container: %s. ID: %v", p.promoteContainerName(), containerID)) - if err := p.dockerClient.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil { + if err := p.dockerClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { return errors.Wrap(err, "failed to start container") } diff --git a/engine/internal/retrieval/engine/postgres/tools/cont/container.go b/engine/internal/retrieval/engine/postgres/tools/cont/container.go index 5baca962..a5d59a0c 100644 --- a/engine/internal/retrieval/engine/postgres/tools/cont/container.go +++ b/engine/internal/retrieval/engine/postgres/tools/cont/container.go @@ -104,7 +104,7 @@ func StopControlContainers(ctx context.Context, dockerClient *client.Client, dbC log.Msg("Removing control container:", containerName) - if err := dockerClient.ContainerRemove(ctx, controlCont.ID, types.ContainerRemoveOptions{ + if err := dockerClient.ContainerRemove(ctx, controlCont.ID, container.RemoveOptions{ RemoveVolumes: true, Force: true, }); err != nil { @@ -141,7 +141,7 @@ func cleanUpContainers(ctx context.Context, dockerCli *client.Client, instanceID for _, controlCont := range list { log.Msg("Removing container:", getContainerName(controlCont)) - if err := dockerCli.ContainerRemove(ctx, controlCont.ID, types.ContainerRemoveOptions{ + if err := dockerCli.ContainerRemove(ctx, controlCont.ID, container.RemoveOptions{ RemoveVolumes: true, Force: true, }); err != nil { @@ -160,7 +160,7 @@ func getContainerList(ctx context.Context, d *client.Client, instanceID string, }, }, pairs...) - return d.ContainerList(ctx, types.ContainerListOptions{ + return d.ContainerList(ctx, container.ListOptions{ Filters: filters.NewArgs(filterPairs...), }) } diff --git a/engine/internal/retrieval/engine/postgres/tools/db/image_content.go b/engine/internal/retrieval/engine/postgres/tools/db/image_content.go index 1f43342e..a66762c6 100644 --- a/engine/internal/retrieval/engine/postgres/tools/db/image_content.go +++ b/engine/internal/retrieval/engine/postgres/tools/db/image_content.go @@ -207,7 +207,7 @@ func createContainer(ctx context.Context, docker *client.Client, image string, p log.Msg(fmt.Sprintf("Running container: %s. ID: %v", containerName, containerID)) - if err := docker.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil { + if err := docker.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { return "", fmt.Errorf("failed to start container %q: %w", containerName, err) } diff --git a/engine/internal/retrieval/engine/postgres/tools/tools.go b/engine/internal/retrieval/engine/postgres/tools/tools.go index 6b196b62..4a0bbe2d 100644 --- a/engine/internal/retrieval/engine/postgres/tools/tools.go +++ b/engine/internal/retrieval/engine/postgres/tools/tools.go @@ -378,7 +378,7 @@ func CheckContainerReadiness(ctx context.Context, dockerClient *client.Client, c // PrintContainerLogs prints container output. func PrintContainerLogs(ctx context.Context, dockerClient *client.Client, containerID string) { - logs, err := dockerClient.ContainerLogs(ctx, containerID, types.ContainerLogsOptions{ + logs, err := dockerClient.ContainerLogs(ctx, containerID, container.LogsOptions{ Since: essentialLogsInterval, ShowStdout: true, ShowStderr: true, @@ -461,7 +461,7 @@ func RemoveContainer(ctx context.Context, dockerClient *client.Client, container log.Msg(fmt.Sprintf("Container %q has been stopped", containerID)) - if err := dockerClient.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{ + if err := dockerClient.ContainerRemove(ctx, containerID, container.RemoveOptions{ RemoveVolumes: true, Force: true, }); err != nil { @@ -638,7 +638,7 @@ func CreateContainerIfMissing(ctx context.Context, docker *client.Client, contai // ListContainersByLabel lists containers by label name and value. func ListContainersByLabel(ctx context.Context, docker *client.Client, filterArgs filters.Args) ([]string, error) { list, err := docker.ContainerList(ctx, - types.ContainerListOptions{ + container.ListOptions{ All: true, Filters: filterArgs, }) @@ -658,7 +658,7 @@ func ListContainersByLabel(ctx context.Context, docker *client.Client, filterArg // CopyContainerLogs collects container logs. func CopyContainerLogs(ctx context.Context, docker *client.Client, containerName, filePath string) error { - reader, err := docker.ContainerLogs(ctx, containerName, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Timestamps: true}) + reader, err := docker.ContainerLogs(ctx, containerName, container.LogsOptions{ShowStdout: true, ShowStderr: true, Timestamps: true}) if err != nil { return err diff --git a/engine/internal/runci/handlers.go b/engine/internal/runci/handlers.go index 8d12dc61..120b795a 100644 --- a/engine/internal/runci/handlers.go +++ b/engine/internal/runci/handlers.go @@ -203,7 +203,7 @@ func (s *Server) runCommands(ctx context.Context, clone *models.Clone, runID str log.Msg(fmt.Sprintf("Running container: %s. ID: %v", containerName, contRunner.ID)) - if err := s.docker.ContainerStart(ctx, contRunner.ID, types.ContainerStartOptions{}); err != nil { + if err := s.docker.ContainerStart(ctx, contRunner.ID, container.StartOptions{}); err != nil { return nil, errors.Wrapf(err, "failed to start container %q", containerName) } diff --git a/engine/internal/srv/ws.go b/engine/internal/srv/ws.go index 9b274c51..64f58211 100644 --- a/engine/internal/srv/ws.go +++ b/engine/internal/srv/ws.go @@ -7,7 +7,7 @@ import ( "net/http" "github.com/ahmetalpbalkan/dlog" - dockTypes "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/gorilla/websocket" "gitlab.com/postgres-ai/database-lab/v3/internal/srv/api" @@ -68,7 +68,7 @@ func (s *Server) instanceLogs(w http.ResponseWriter, r *http.Request) { } }() - readCloser, err := s.docker.ContainerLogs(r.Context(), s.engProps.ContainerName, dockTypes.ContainerLogsOptions{ + readCloser, err := s.docker.ContainerLogs(r.Context(), s.engProps.ContainerName, container.LogsOptions{ ShowStdout: true, ShowStderr: true, Since: logsSinceInterval, diff --git a/engine/pkg/util/projection/operations.go b/engine/pkg/util/projection/operations.go index db12ac88..7e966b7b 100644 --- a/engine/pkg/util/projection/operations.go +++ b/engine/pkg/util/projection/operations.go @@ -35,6 +35,7 @@ func Load(target interface{}, accessor Accessor, options LoadOptions) error { } else { field.Set(reflect.ValueOf(accessorValue)) } + return nil }, ) @@ -46,22 +47,28 @@ func Store(target interface{}, accessor Accessor, options StoreOptions) error { if !tag.matchesStore(options) { return nil } + var accessorValue interface{} + if tag.isPtr { if field.IsNil() { return nil } + accessorValue = field.Elem().Interface() } else { accessorValue = field.Interface() } + err := accessor.Set(FieldSet{ Path: tag.path, Value: accessorValue, Type: tag.fType, CreateKey: tag.createKey, }) + if err != nil { return err } + return nil }, ) From fb851b5a9254ddaeeb24672772690d854b249030 Mon Sep 17 00:00:00 2001 From: Artyom Kartasov Date: Sun, 29 Sep 2024 13:23:06 +0000 Subject: [PATCH 070/111] chore: update UI dependencies to fix vulnerabilities 2024-09 --- ui/package.json | 6 +- ui/pnpm-lock.yaml | 465 ++++++++++++++++++++++++++++------------------ 2 files changed, 285 insertions(+), 186 deletions(-) diff --git a/ui/package.json b/ui/package.json index 9a63bf0c..82b2d887 100644 --- a/ui/package.json +++ b/ui/package.json @@ -45,7 +45,11 @@ "express@<4.19.2": ">=4.19.2", "follow-redirects@<=1.15.5": ">=1.15.6", "@babel/traverse@<7.23.2": ">=7.23.2", - "@pmmmwh/react-refresh-webpack-plugin@<0.5.13": ">=0.5.13" + "bootstrap@>=4.0.0 <=4.6.2": ">=5.0.0", + "elliptic@>=4.0.0 <=6.5.6": ">=6.5.7", + "elliptic@>=2.0.0 <=6.5.6": ">=6.5.7", + "elliptic@>=5.2.1 <=6.5.6": ">=6.5.7", + "dompurify@<2.5.4": ">=2.5.4" } } } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 5ce19d71..db0ffb93 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -38,7 +38,11 @@ overrides: express@<4.19.2: '>=4.19.2' follow-redirects@<=1.15.5: '>=1.15.6' '@babel/traverse@<7.23.2': '>=7.23.2' - '@pmmmwh/react-refresh-webpack-plugin@<0.5.13': '>=0.5.13' + bootstrap@>=4.0.0 <=4.6.2: '>=5.0.0' + elliptic@>=4.0.0 <=6.5.6: '>=6.5.7' + elliptic@>=2.0.0 <=6.5.6: '>=6.5.7' + elliptic@>=5.2.1 <=6.5.6: '>=6.5.7' + dompurify@<2.5.4: '>=2.5.4' importers: @@ -306,8 +310,8 @@ importers: specifier: ^15.5.6 version: 15.5.6 bootstrap: - specifier: ^4.3.1 - version: 4.6.0(jquery@3.7.1)(popper.js@1.16.1) + specifier: '>=5.0.0' + version: 5.3.3(@popperjs/core@2.11.8) byte-size: specifier: ^7.0.1 version: 7.0.1 @@ -336,8 +340,8 @@ importers: specifier: ^2.22.1 version: 2.25.0 dompurify: - specifier: ^2.0.12 - version: 2.3.3 + specifier: '>=2.5.4' + version: 3.1.6 dotenv: specifier: ^10.0.0 version: 10.0.0 @@ -2763,7 +2767,7 @@ packages: jest-util: 27.5.1 jest-validate: 27.5.1 jest-watcher: 27.5.1 - micromatch: 4.0.5 + micromatch: 4.0.8 rimraf: 3.0.2 slash: 3.0.0 strip-ansi: 6.0.1 @@ -2906,7 +2910,7 @@ packages: jest-haste-map: 27.5.1 jest-regex-util: 27.5.1 jest-util: 27.5.1 - micromatch: 4.0.5 + micromatch: 4.0.8 pirates: 4.0.5 slash: 3.0.0 source-map: 0.6.1 @@ -3341,7 +3345,7 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.13.0 - /@pmmmwh/react-refresh-webpack-plugin@0.5.13(react-refresh@0.11.0)(webpack-dev-server@4.11.0)(webpack@5.91.0): + /@pmmmwh/react-refresh-webpack-plugin@0.5.13(react-refresh@0.11.0)(webpack-dev-server@4.11.0)(webpack@5.95.0): resolution: {integrity: sha512-odZVYXly+JwzYri9rKqqUAk0cY6zLpv4dxoKinhoJNShV36Gpxf+CyDIILJ4tYsJ1ZxIWs233Y39iVnynvDA/g==} engines: {node: '>= 10.13'} peerDependencies: @@ -3375,15 +3379,19 @@ packages: react-refresh: 0.11.0 schema-utils: 3.3.0 source-map: 0.7.4 - webpack: 5.91.0 - webpack-dev-server: 4.11.0(webpack@5.91.0) + webpack: 5.95.0 + webpack-dev-server: 4.11.0(webpack@5.95.0) dev: false /@popperjs/core@2.11.6: resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} dev: false - /@rollup/plugin-babel@5.3.1(@babel/core@7.19.0)(rollup@2.79.0): + /@popperjs/core@2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + dev: false + + /@rollup/plugin-babel@5.3.1(@babel/core@7.19.0)(rollup@2.79.2): resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} engines: {node: '>= 10.0.0'} peerDependencies: @@ -3396,36 +3404,36 @@ packages: dependencies: '@babel/core': 7.19.0 '@babel/helper-module-imports': 7.24.3 - '@rollup/pluginutils': 3.1.0(rollup@2.79.0) - rollup: 2.79.0 + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) + rollup: 2.79.2 dev: false - /@rollup/plugin-node-resolve@11.2.1(rollup@2.79.0): + /@rollup/plugin-node-resolve@11.2.1(rollup@2.79.2): resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} engines: {node: '>= 10.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0 dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.79.0) + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) '@types/resolve': 1.17.1 builtin-modules: 3.2.0 deepmerge: 4.2.2 is-module: 1.0.0 resolve: 1.22.1 - rollup: 2.79.0 + rollup: 2.79.2 dev: false - /@rollup/plugin-replace@2.4.2(rollup@2.79.0): + /@rollup/plugin-replace@2.4.2(rollup@2.79.2): resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} peerDependencies: rollup: ^1.20.0 || ^2.0.0 dependencies: - '@rollup/pluginutils': 3.1.0(rollup@2.79.0) + '@rollup/pluginutils': 3.1.0(rollup@2.79.2) magic-string: 0.25.9 - rollup: 2.79.0 + rollup: 2.79.2 dev: false - /@rollup/pluginutils@3.1.0(rollup@2.79.0): + /@rollup/pluginutils@3.1.0(rollup@2.79.2): resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} engines: {node: '>= 8.0.0'} peerDependencies: @@ -3434,7 +3442,7 @@ packages: '@types/estree': 0.0.39 estree-walker: 1.0.1 picomatch: 2.3.1 - rollup: 2.79.0 + rollup: 2.79.2 dev: false /@rushstack/eslint-patch@1.1.4: @@ -3584,7 +3592,7 @@ packages: /@surma/rollup-plugin-off-main-thread@2.2.3: resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} dependencies: - ejs: 3.1.8 + ejs: 3.1.10 json5: 2.2.3 magic-string: 0.25.9 string.prototype.matchall: 4.0.7 @@ -3979,13 +3987,6 @@ packages: '@types/trusted-types': 2.0.2 dev: false - /@types/eslint-scope@3.7.4: - resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} - dependencies: - '@types/eslint': 8.4.6 - '@types/estree': 1.0.5 - dev: false - /@types/eslint@8.4.6: resolution: {integrity: sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==} dependencies: @@ -4533,8 +4534,8 @@ packages: acorn-walk: 7.2.0 dev: false - /acorn-import-assertions@1.9.0(acorn@8.11.3): - resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + /acorn-import-attributes@1.9.5(acorn@8.11.3): + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: acorn: ^8 dependencies: @@ -4901,7 +4902,7 @@ packages: - supports-color dev: false - /babel-loader@9.1.3(@babel/core@7.19.0)(webpack@5.91.0): + /babel-loader@9.1.3(@babel/core@7.19.0)(webpack@5.95.0): resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==} engines: {node: '>= 14.15.0'} peerDependencies: @@ -4911,7 +4912,7 @@ packages: '@babel/core': 7.19.0 find-cache-dir: 4.0.0 schema-utils: 4.0.0 - webpack: 5.91.0 + webpack: 5.95.0 dev: false /babel-plugin-dynamic-import-node@2.3.3: @@ -5118,8 +5119,8 @@ packages: resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} dev: false - /body-parser@1.20.2: - resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + /body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} dependencies: bytes: 3.1.2 @@ -5130,7 +5131,7 @@ packages: http-errors: 2.0.0 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.11.0 + qs: 6.13.0 raw-body: 2.5.2 type-is: 1.6.18 unpipe: 1.0.0 @@ -5151,14 +5152,12 @@ packages: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} dev: false - /bootstrap@4.6.0(jquery@3.7.1)(popper.js@1.16.1): - resolution: {integrity: sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==} + /bootstrap@5.3.3(@popperjs/core@2.11.8): + resolution: {integrity: sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==} peerDependencies: - jquery: 1.9.1 - 3 - popper.js: ^1.16.1 + '@popperjs/core': ^2.11.8 dependencies: - jquery: 3.7.1 - popper.js: 1.16.1 + '@popperjs/core': 2.11.8 dev: false /brace-expansion@1.1.11: @@ -5173,11 +5172,11 @@ packages: balanced-match: 1.0.2 dev: false - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} dependencies: - fill-range: 7.0.1 + fill-range: 7.1.1 /brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} @@ -5230,7 +5229,7 @@ packages: browserify-rsa: 4.1.0 create-hash: 1.2.0 create-hmac: 1.1.7 - elliptic: 6.5.5 + elliptic: 6.5.7 hash-base: 3.0.4 inherits: 2.0.4 parse-asn1: 5.1.7 @@ -5313,6 +5312,17 @@ packages: function-bind: 1.1.1 get-intrinsic: 1.1.2 + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + dev: false + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -5449,7 +5459,7 @@ packages: requiresBuild: true dependencies: anymatch: 3.1.2 - braces: 3.0.2 + braces: 3.0.3 glob-parent: 5.1.2 is-binary-path: 2.1.0 is-glob: 4.0.3 @@ -5829,7 +5839,7 @@ packages: resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} dependencies: bn.js: 4.12.0 - elliptic: 6.5.5 + elliptic: 6.5.7 dev: false /create-file-webpack@1.0.2: @@ -5909,7 +5919,7 @@ packages: resolution: {integrity: sha512-TmP1bSMNS0TpqHgrE4vp4NhDM+MSL75JaqWWfGiCBlutOTYjn3D1+btDQFfDAph/C1PCdc8YFrqqv+72MjoONQ==} engines: {node: '>=12.13.0'} dependencies: - micromatch: 4.0.5 + micromatch: 4.0.8 dev: true /cspell-io@5.18.4: @@ -6004,7 +6014,7 @@ packages: postcss-selector-parser: 6.0.10 dev: false - /css-loader@6.7.1(webpack@5.91.0): + /css-loader@6.7.1(webpack@5.95.0): resolution: {integrity: sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==} engines: {node: '>= 12.13.0'} peerDependencies: @@ -6018,10 +6028,10 @@ packages: postcss-modules-values: 4.0.0(postcss@8.4.38) postcss-value-parser: 4.2.0 semver: 7.5.4 - webpack: 5.91.0 + webpack: 5.95.0 dev: false - /css-minimizer-webpack-plugin@3.4.1(webpack@5.91.0): + /css-minimizer-webpack-plugin@3.4.1(webpack@5.95.0): resolution: {integrity: sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==} engines: {node: '>= 12.13.0'} peerDependencies: @@ -6046,7 +6056,7 @@ packages: schema-utils: 4.0.0 serialize-javascript: 6.0.2 source-map: 0.6.1 - webpack: 5.91.0 + webpack: 5.95.0 dev: false /css-prefers-color-scheme@6.0.3(postcss@8.4.38): @@ -6881,6 +6891,15 @@ packages: execa: 5.1.1 dev: false + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + dev: false + /define-lazy-prop@2.0.0: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} @@ -7085,10 +7104,6 @@ packages: domelementtype: 2.3.0 dev: false - /dompurify@2.3.3: - resolution: {integrity: sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg==} - dev: false - /dompurify@3.1.6: resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==} dev: false @@ -7151,8 +7166,8 @@ packages: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false - /ejs@3.1.8: - resolution: {integrity: sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==} + /ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} hasBin: true dependencies: @@ -7162,8 +7177,8 @@ packages: /electron-to-chromium@1.4.749: resolution: {integrity: sha512-LRMMrM9ITOvue0PoBrvNIraVmuDbJV5QC9ierz/z5VilMdPOVMjOtpICNld3PuXuTZ3CHH/UPxX9gHhAPwi+0Q==} - /elliptic@6.5.5: - resolution: {integrity: sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==} + /elliptic@6.5.7: + resolution: {integrity: sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==} dependencies: bn.js: 4.12.0 brorand: 1.1.0 @@ -7201,14 +7216,19 @@ packages: engines: {node: '>= 0.8'} dev: false + /encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + dev: false + /end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: once: 1.4.0 dev: false - /enhanced-resolve@5.16.0: - resolution: {integrity: sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==} + /enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} dependencies: graceful-fs: 4.2.11 @@ -7273,6 +7293,18 @@ packages: resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} dev: false + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + dev: false + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + dev: false + /es-module-lexer@1.5.0: resolution: {integrity: sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==} dev: false @@ -7584,7 +7616,7 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - /eslint-webpack-plugin@3.2.0(eslint@8.57.0)(webpack@5.91.0): + /eslint-webpack-plugin@3.2.0(eslint@8.57.0)(webpack@5.95.0): resolution: {integrity: sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==} engines: {node: '>= 12.13.0'} peerDependencies: @@ -7594,10 +7626,10 @@ packages: '@types/eslint': 8.4.6 eslint: 8.57.0 jest-worker: 28.1.3 - micromatch: 4.0.5 + micromatch: 4.0.8 normalize-path: 3.0.0 schema-utils: 4.0.0 - webpack: 5.91.0 + webpack: 5.95.0 dev: false /eslint@8.57.0: @@ -7775,36 +7807,36 @@ packages: jest-message-util: 27.5.1 dev: false - /express@4.19.2: - resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} + /express@4.21.0: + resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==} engines: {node: '>= 0.10.0'} dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.2 + body-parser: 1.20.3 content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.6.0 cookie-signature: 1.0.6 debug: 2.6.9 depd: 2.0.0 - encodeurl: 1.0.2 + encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.2.0 + finalhandler: 1.3.1 fresh: 0.5.2 http-errors: 2.0.0 - merge-descriptors: 1.0.1 + merge-descriptors: 1.0.3 methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 - path-to-regexp: 0.1.7 + path-to-regexp: 0.1.10 proxy-addr: 2.0.7 - qs: 6.11.0 + qs: 6.13.0 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.18.0 - serve-static: 1.15.0 + send: 0.19.0 + serve-static: 1.16.2 setprototypeof: 1.2.0 statuses: 2.0.1 type-is: 1.6.18 @@ -7855,7 +7887,7 @@ packages: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.5 + micromatch: 4.0.8 /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -7911,7 +7943,7 @@ packages: dependencies: flat-cache: 3.0.4 - /file-loader@6.2.0(webpack@5.91.0): + /file-loader@6.2.0(webpack@5.95.0): resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} engines: {node: '>= 10.13.0'} peerDependencies: @@ -7919,7 +7951,7 @@ packages: dependencies: loader-utils: 3.2.1 schema-utils: 3.3.0 - webpack: 5.91.0 + webpack: 5.95.0 dev: false /filelist@1.0.4: @@ -7933,18 +7965,18 @@ packages: engines: {node: '>= 0.4.0'} dev: false - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 - /finalhandler@1.2.0: - resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + /finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} dependencies: debug: 2.6.9 - encodeurl: 1.0.2 + encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 @@ -8019,7 +8051,7 @@ packages: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} dev: false - /fork-ts-checker-webpack-plugin@6.5.2(eslint@8.57.0)(typescript@4.8.3)(webpack@5.91.0): + /fork-ts-checker-webpack-plugin@6.5.2(eslint@8.57.0)(typescript@4.8.3)(webpack@5.95.0): resolution: {integrity: sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==} engines: {node: '>=10', yarn: '>=1.0.0'} peerDependencies: @@ -8048,7 +8080,7 @@ packages: semver: 7.5.4 tapable: 1.1.3 typescript: 4.8.3 - webpack: 5.91.0 + webpack: 5.95.0 dev: false /form-data@2.3.3: @@ -8138,6 +8170,10 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + /function.prototype.name@1.1.5: resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} engines: {node: '>= 0.4'} @@ -8174,6 +8210,17 @@ packages: has: 1.0.3 has-symbols: 1.0.3 + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + dev: false + /get-own-enumerable-property-symbols@3.0.2: resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} dev: false @@ -8312,6 +8359,12 @@ packages: minimist: 1.2.8 dev: true + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.4 + dev: false + /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -8363,6 +8416,17 @@ packages: dependencies: get-intrinsic: 1.1.2 + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + dev: false + + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + dev: false + /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} @@ -8403,6 +8467,13 @@ packages: minimalistic-assert: 1.0.1 dev: false + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + /hast-to-hyperscript@10.0.1: resolution: {integrity: sha512-dhIVGoKCQVewFi+vz3Vt567E4ejMppS1haBRL6TEmeLeJVB1i/FJIIg/e6s1Bwn0g5qtYojHEKvyGA+OZuyifw==} dependencies: @@ -8597,7 +8668,7 @@ packages: resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} dev: false - /html-webpack-plugin@5.5.0(webpack@5.91.0): + /html-webpack-plugin@5.5.0(webpack@5.95.0): resolution: {integrity: sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==} engines: {node: '>=10.13.0'} peerDependencies: @@ -8608,7 +8679,7 @@ packages: lodash: 4.17.21 pretty-error: 4.0.0 tapable: 2.2.1 - webpack: 5.91.0 + webpack: 5.95.0 dev: false /htmlparser2@3.10.1: @@ -8685,7 +8756,7 @@ packages: http-proxy: 1.18.1 is-glob: 4.0.3 is-plain-obj: 3.0.0 - micromatch: 4.0.5 + micromatch: 4.0.8 transitivePeerDependencies: - debug dev: false @@ -9268,7 +9339,7 @@ packages: jest-runner: 27.5.1 jest-util: 27.5.1 jest-validate: 27.5.1 - micromatch: 4.0.5 + micromatch: 4.0.8 parse-json: 5.2.0 pretty-format: 27.5.1 slash: 3.0.0 @@ -9357,7 +9428,7 @@ packages: jest-serializer: 27.5.1 jest-util: 27.5.1 jest-worker: 27.5.1 - micromatch: 4.0.5 + micromatch: 4.0.8 walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 @@ -9415,7 +9486,7 @@ packages: '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.11 - micromatch: 4.0.5 + micromatch: 4.0.8 pretty-format: 27.5.1 slash: 3.0.0 stack-utils: 2.0.5 @@ -9430,7 +9501,7 @@ packages: '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.11 - micromatch: 4.0.5 + micromatch: 4.0.8 pretty-format: 28.1.3 slash: 3.0.0 stack-utils: 2.0.5 @@ -9720,10 +9791,6 @@ packages: - utf-8-validate dev: false - /jquery@3.7.1: - resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} - dev: false - /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -9779,7 +9846,7 @@ packages: whatwg-encoding: 1.0.5 whatwg-mimetype: 2.3.0 whatwg-url: 8.7.0 - ws: 7.5.9 + ws: 7.5.10 xml-name-validator: 3.0.0 transitivePeerDependencies: - bufferutil @@ -10432,8 +10499,8 @@ packages: yargs-parser: 20.2.9 dev: true - /merge-descriptors@1.0.1: - resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + /merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} dev: false /merge-stream@2.0.0: @@ -10729,11 +10796,11 @@ packages: - supports-color dev: false - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + /micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} dependencies: - braces: 3.0.2 + braces: 3.0.3 picomatch: 2.3.1 /miller-rabin@4.0.1: @@ -10785,14 +10852,14 @@ packages: tiny-warning: 1.0.3 dev: false - /mini-css-extract-plugin@2.6.1(webpack@5.91.0): + /mini-css-extract-plugin@2.6.1(webpack@5.95.0): resolution: {integrity: sha512-wd+SD57/K6DiV7jIR34P+s3uckTRuQvx0tKPcvjFlrEylk6P4mQ2KSWk1hblj1Kxaqok7LogKOieygXqBczNlg==} engines: {node: '>= 12.13.0'} peerDependencies: webpack: '>=5.76.0' dependencies: schema-utils: 4.0.0 - webpack: 5.91.0 + webpack: 5.95.0 dev: false /minimalistic-assert@1.0.1: @@ -11003,6 +11070,11 @@ packages: /object-inspect@1.12.2: resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} + /object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} + dev: false + /object-keys@0.4.0: resolution: {integrity: sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==} dev: false @@ -11284,12 +11356,12 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - /path-to-regexp@0.1.7: - resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + /path-to-regexp@0.1.10: + resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} dev: false - /path-to-regexp@1.8.0: - resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} + /path-to-regexp@1.9.0: + resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} dependencies: isarray: 0.0.1 dev: false @@ -11377,11 +11449,6 @@ packages: points-on-curve: 0.2.0 dev: false - /popper.js@1.16.1: - resolution: {integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==} - deprecated: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1 - dev: false - /popper.js@1.16.1-lts: resolution: {integrity: sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==} dev: false @@ -11708,7 +11775,7 @@ packages: yaml: 1.10.2 dev: false - /postcss-loader@6.2.1(postcss@8.4.38)(webpack@5.91.0): + /postcss-loader@6.2.1(postcss@8.4.38)(webpack@5.95.0): resolution: {integrity: sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==} engines: {node: '>= 12.13.0'} peerDependencies: @@ -11719,7 +11786,7 @@ packages: klona: 2.0.5 postcss: 8.4.38 semver: 7.5.4 - webpack: 5.91.0 + webpack: 5.95.0 dev: false /postcss-logical@5.0.4(postcss@8.4.38): @@ -12414,6 +12481,13 @@ packages: side-channel: 1.0.4 dev: false + /qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + dev: false + /querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} dev: false @@ -12507,7 +12581,7 @@ packages: react: 17.0.2 dev: false - /react-dev-utils@12.0.1(eslint@8.57.0)(typescript@4.8.3)(webpack@5.91.0): + /react-dev-utils@12.0.1(eslint@8.57.0)(typescript@4.8.3)(webpack@5.95.0): resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} engines: {node: '>=14'} peerDependencies: @@ -12526,7 +12600,7 @@ packages: escape-string-regexp: 4.0.0 filesize: 8.0.7 find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 6.5.2(eslint@8.57.0)(typescript@4.8.3)(webpack@5.91.0) + fork-ts-checker-webpack-plugin: 6.5.2(eslint@8.57.0)(typescript@4.8.3)(webpack@5.95.0) global-modules: 2.0.0 globby: 11.1.0 gzip-size: 6.0.0 @@ -12542,7 +12616,7 @@ packages: strip-ansi: 6.0.1 text-table: 0.2.0 typescript: 4.8.3 - webpack: 5.91.0 + webpack: 5.95.0 transitivePeerDependencies: - eslint - supports-color @@ -12684,7 +12758,7 @@ packages: hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 mini-create-react-context: 0.4.1(prop-types@15.8.1)(react@17.0.2) - path-to-regexp: 1.8.0 + path-to-regexp: 1.9.0 prop-types: 15.8.1 react: 17.0.2 react-is: 16.13.1 @@ -12705,54 +12779,54 @@ packages: optional: true dependencies: '@babel/core': 7.19.0 - '@pmmmwh/react-refresh-webpack-plugin': 0.5.13(react-refresh@0.11.0)(webpack-dev-server@4.11.0)(webpack@5.91.0) + '@pmmmwh/react-refresh-webpack-plugin': 0.5.13(react-refresh@0.11.0)(webpack-dev-server@4.11.0)(webpack@5.95.0) '@svgr/webpack': 5.5.0 babel-jest: 27.5.1(@babel/core@7.19.0) - babel-loader: 9.1.3(@babel/core@7.19.0)(webpack@5.91.0) + babel-loader: 9.1.3(@babel/core@7.19.0)(webpack@5.95.0) babel-plugin-named-asset-import: 0.3.8(@babel/core@7.19.0) babel-preset-react-app: 10.0.1 bfj: 7.0.2 browserslist: 4.23.0 camelcase: 6.3.0 case-sensitive-paths-webpack-plugin: 2.4.0 - css-loader: 6.7.1(webpack@5.91.0) - css-minimizer-webpack-plugin: 3.4.1(webpack@5.91.0) + css-loader: 6.7.1(webpack@5.95.0) + css-minimizer-webpack-plugin: 3.4.1(webpack@5.95.0) dotenv: 10.0.0 dotenv-expand: 5.1.0 eslint: 8.57.0 eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.24.1)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.57.0)(jest@27.5.1)(typescript@4.8.3) - eslint-webpack-plugin: 3.2.0(eslint@8.57.0)(webpack@5.91.0) - file-loader: 6.2.0(webpack@5.91.0) + eslint-webpack-plugin: 3.2.0(eslint@8.57.0)(webpack@5.95.0) + file-loader: 6.2.0(webpack@5.95.0) fs-extra: 10.1.0 - html-webpack-plugin: 5.5.0(webpack@5.91.0) + html-webpack-plugin: 5.5.0(webpack@5.95.0) identity-obj-proxy: 3.0.0 jest: 27.5.1 jest-resolve: 27.5.1 jest-watch-typeahead: 1.1.0(jest@27.5.1) - mini-css-extract-plugin: 2.6.1(webpack@5.91.0) + mini-css-extract-plugin: 2.6.1(webpack@5.95.0) postcss: 8.4.38 postcss-flexbugs-fixes: 5.0.2(postcss@8.4.38) - postcss-loader: 6.2.1(postcss@8.4.38)(webpack@5.91.0) + postcss-loader: 6.2.1(postcss@8.4.38)(webpack@5.95.0) postcss-normalize: 10.0.1(browserslist@4.23.0)(postcss@8.4.38) postcss-preset-env: 7.8.1(postcss@8.4.38) prompts: 2.4.2 react: 17.0.2 react-app-polyfill: 3.0.0 - react-dev-utils: 12.0.1(eslint@8.57.0)(typescript@4.8.3)(webpack@5.91.0) + react-dev-utils: 12.0.1(eslint@8.57.0)(typescript@4.8.3)(webpack@5.95.0) react-refresh: 0.11.0 resolve: 1.22.1 resolve-url-loader: 5.0.0 - sass-loader: 12.6.0(sass@1.43.2)(webpack@5.91.0) + sass-loader: 12.6.0(sass@1.43.2)(webpack@5.95.0) semver: 7.5.4 - source-map-loader: 3.0.1(webpack@5.91.0) - style-loader: 3.3.1(webpack@5.91.0) + source-map-loader: 3.0.1(webpack@5.95.0) + style-loader: 3.3.1(webpack@5.95.0) tailwindcss: 3.1.8(postcss@8.4.38) - terser-webpack-plugin: 5.3.10(webpack@5.91.0) + terser-webpack-plugin: 5.3.10(webpack@5.95.0) typescript: 4.8.3 - webpack: 5.91.0 - webpack-dev-server: 4.11.0(webpack@5.91.0) - webpack-manifest-plugin: 4.1.1(webpack@5.91.0) - workbox-webpack-plugin: 6.5.4(webpack@5.91.0) + webpack: 5.95.0 + webpack-dev-server: 4.11.0(webpack@5.95.0) + webpack-manifest-plugin: 4.1.1(webpack@5.95.0) + workbox-webpack-plugin: 6.5.4(webpack@5.95.0) optionalDependencies: fsevents: 2.3.3 transitivePeerDependencies: @@ -13202,7 +13276,7 @@ packages: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} dev: false - /rollup-plugin-terser@7.0.2(rollup@2.79.0): + /rollup-plugin-terser@7.0.2(rollup@2.79.2): resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser peerDependencies: @@ -13210,13 +13284,13 @@ packages: dependencies: '@babel/code-frame': 7.24.2 jest-worker: 26.6.2 - rollup: 2.79.0 + rollup: 2.79.2 serialize-javascript: 4.0.0 terser: 5.30.4 dev: false - /rollup@2.79.0: - resolution: {integrity: sha512-x4KsrCgwQ7ZJPcFA/SUu6QVcYlO7uRLfLAy0DSA4NS2eG8japdbpM50ToH7z4iObodRYOJ0soneF0iaQRJ6zhA==} + /rollup@2.79.2: + resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} engines: {node: '>=10.0.0'} hasBin: true optionalDependencies: @@ -13268,7 +13342,7 @@ packages: resolution: {integrity: sha512-vTxrZz4dX5W86M6oVWVdOVe72ZiPs41Oi7Z6Km4W5Turyz28mrXSJhhEBZoRtzJWIv3833WKVwLSDWWkEfupMg==} dev: false - /sass-loader@12.6.0(sass@1.43.2)(webpack@5.91.0): + /sass-loader@12.6.0(sass@1.43.2)(webpack@5.95.0): resolution: {integrity: sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==} engines: {node: '>= 12.13.0'} peerDependencies: @@ -13290,7 +13364,7 @@ packages: klona: 2.0.5 neo-async: 2.6.2 sass: 1.43.2 - webpack: 5.91.0 + webpack: 5.95.0 dev: false /sass@1.43.2: @@ -13364,8 +13438,8 @@ packages: dependencies: lru-cache: 6.0.0 - /send@0.18.0: - resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + /send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} dependencies: debug: 2.6.9 @@ -13412,18 +13486,30 @@ packages: - supports-color dev: false - /serve-static@1.15.0: - resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + /serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} dependencies: - encodeurl: 1.0.2 + encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.18.0 + send: 0.19.0 transitivePeerDependencies: - supports-color dev: false + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + dev: false + /setprototypeof@1.1.0: resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} dev: false @@ -13468,6 +13554,16 @@ packages: get-intrinsic: 1.1.2 object-inspect: 1.12.2 + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.2 + dev: false + /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -13517,7 +13613,7 @@ packages: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} - /source-map-loader@3.0.1(webpack@5.91.0): + /source-map-loader@3.0.1(webpack@5.95.0): resolution: {integrity: sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA==} engines: {node: '>= 12.13.0'} peerDependencies: @@ -13526,7 +13622,7 @@ packages: abab: 2.0.6 iconv-lite: 0.6.3 source-map-js: 1.2.0 - webpack: 5.91.0 + webpack: 5.95.0 dev: false /source-map-support@0.5.21: @@ -13803,13 +13899,13 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - /style-loader@3.3.1(webpack@5.91.0): + /style-loader@3.3.1(webpack@5.95.0): resolution: {integrity: sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==} engines: {node: '>= 12.13.0'} peerDependencies: webpack: '>=5.76.0' dependencies: - webpack: 5.91.0 + webpack: 5.95.0 dev: false /style-search@0.1.0: @@ -13965,7 +14061,7 @@ packages: log-symbols: 4.1.0 mathml-tag-names: 2.1.3 meow: 9.0.0 - micromatch: 4.0.5 + micromatch: 4.0.8 normalize-selector: 0.2.0 postcss: 8.4.38 postcss-html: 0.36.0(postcss-syntax@0.36.2)(postcss@8.4.38) @@ -14019,7 +14115,7 @@ packages: known-css-properties: 0.23.0 mathml-tag-names: 2.1.3 meow: 9.0.0 - micromatch: 4.0.5 + micromatch: 4.0.8 normalize-path: 3.0.0 normalize-selector: 0.2.0 picocolors: 1.0.0 @@ -14210,7 +14306,7 @@ packages: supports-hyperlinks: 2.3.0 dev: false - /terser-webpack-plugin@5.3.10(webpack@5.91.0): + /terser-webpack-plugin@5.3.10(webpack@5.95.0): resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} engines: {node: '>= 10.13.0'} peerDependencies: @@ -14231,7 +14327,7 @@ packages: schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.30.4 - webpack: 5.91.0 + webpack: 5.95.0 dev: false /terser@5.30.4: @@ -14953,7 +15049,7 @@ packages: engines: {node: '>=10.4'} dev: false - /webpack-dev-middleware@7.2.1(webpack@5.91.0): + /webpack-dev-middleware@7.2.1(webpack@5.95.0): resolution: {integrity: sha512-hRLz+jPQXo999Nx9fXVdKlg/aehsw1ajA9skAneGmT03xwmyuhvF93p6HUKKbWhXdcERtGTzUCtIQr+2IQegrA==} engines: {node: '>= 18.12.0'} peerDependencies: @@ -14968,10 +15064,10 @@ packages: on-finished: 2.4.1 range-parser: 1.2.1 schema-utils: 4.0.0 - webpack: 5.91.0 + webpack: 5.95.0 dev: false - /webpack-dev-server@4.11.0(webpack@5.91.0): + /webpack-dev-server@4.11.0(webpack@5.95.0): resolution: {integrity: sha512-L5S4Q2zT57SK7tazgzjMiSMBdsw+rGYIX27MgPgx7LDhWO0lViPrHKoLS7jo5In06PWYAhlYu3PbyoC6yAThbw==} engines: {node: '>= 12.13.0'} hasBin: true @@ -14996,7 +15092,7 @@ packages: compression: 1.7.4 connect-history-api-fallback: 2.0.0 default-gateway: 6.0.3 - express: 4.19.2 + express: 4.21.0 graceful-fs: 4.2.11 html-entities: 2.3.3 http-proxy-middleware: 2.0.6(@types/express@4.17.13) @@ -15009,9 +15105,9 @@ packages: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack: 5.91.0 - webpack-dev-middleware: 7.2.1(webpack@5.91.0) - ws: 8.8.1 + webpack: 5.95.0 + webpack-dev-middleware: 7.2.1(webpack@5.95.0) + ws: 8.18.0 transitivePeerDependencies: - bufferutil - debug @@ -15019,14 +15115,14 @@ packages: - utf-8-validate dev: false - /webpack-manifest-plugin@4.1.1(webpack@5.91.0): + /webpack-manifest-plugin@4.1.1(webpack@5.95.0): resolution: {integrity: sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==} engines: {node: '>=12.22.0'} peerDependencies: webpack: '>=5.76.0' dependencies: tapable: 2.2.1 - webpack: 5.91.0 + webpack: 5.95.0 webpack-sources: 2.3.1 dev: false @@ -15064,8 +15160,8 @@ packages: engines: {node: '>=10.13.0'} dev: false - /webpack@5.91.0: - resolution: {integrity: sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==} + /webpack@5.95.0: + resolution: {integrity: sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -15074,16 +15170,15 @@ packages: webpack-cli: optional: true dependencies: - '@types/eslint-scope': 3.7.4 '@types/estree': 1.0.5 '@webassemblyjs/ast': 1.12.1 '@webassemblyjs/wasm-edit': 1.12.1 '@webassemblyjs/wasm-parser': 1.12.1 acorn: 8.11.3 - acorn-import-assertions: 1.9.0(acorn@8.11.3) + acorn-import-attributes: 1.9.5(acorn@8.11.3) browserslist: 4.23.0 chrome-trace-event: 1.0.3 - enhanced-resolve: 5.16.0 + enhanced-resolve: 5.17.1 es-module-lexer: 1.5.0 eslint-scope: 5.1.1 events: 3.3.0 @@ -15095,7 +15190,7 @@ packages: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(webpack@5.91.0) + terser-webpack-plugin: 5.3.10(webpack@5.95.0) watchpack: 2.4.1 webpack-sources: 3.2.3 transitivePeerDependencies: @@ -15201,9 +15296,9 @@ packages: '@babel/core': 7.19.0 '@babel/preset-env': 7.19.0(@babel/core@7.19.0) '@babel/runtime': 7.19.0 - '@rollup/plugin-babel': 5.3.1(@babel/core@7.19.0)(rollup@2.79.0) - '@rollup/plugin-node-resolve': 11.2.1(rollup@2.79.0) - '@rollup/plugin-replace': 2.4.2(rollup@2.79.0) + '@rollup/plugin-babel': 5.3.1(@babel/core@7.19.0)(rollup@2.79.2) + '@rollup/plugin-node-resolve': 11.2.1(rollup@2.79.2) + '@rollup/plugin-replace': 2.4.2(rollup@2.79.2) '@surma/rollup-plugin-off-main-thread': 2.2.3 ajv: 8.11.0 common-tags: 1.8.2 @@ -15212,8 +15307,8 @@ packages: glob: 7.2.3 lodash: 4.17.21 pretty-bytes: 5.6.0 - rollup: 2.79.0 - rollup-plugin-terser: 7.0.2(rollup@2.79.0) + rollup: 2.79.2 + rollup-plugin-terser: 7.0.2(rollup@2.79.2) source-map: 0.8.0-beta.0 stringify-object: 3.3.0 strip-comments: 2.0.1 @@ -15319,7 +15414,7 @@ packages: resolution: {integrity: sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==} dev: false - /workbox-webpack-plugin@6.5.4(webpack@5.91.0): + /workbox-webpack-plugin@6.5.4(webpack@5.95.0): resolution: {integrity: sha512-LmWm/zoaahe0EGmMTrSLUi+BjyR3cdGEfU3fS6PN1zKFYbqAKuQ+Oy/27e4VSXsyIwAw8+QDfk1XHNGtZu9nQg==} engines: {node: '>=10.0.0'} peerDependencies: @@ -15328,7 +15423,7 @@ packages: fast-json-stable-stringify: 2.1.0 pretty-bytes: 5.6.0 upath: 1.2.0 - webpack: 5.91.0 + webpack: 5.95.0 webpack-sources: 1.4.3 workbox-build: 6.5.4 transitivePeerDependencies: @@ -15379,8 +15474,8 @@ packages: mkdirp: 0.5.6 dev: false - /ws@7.5.9: - resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + /ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} peerDependencies: bufferutil: ^4.0.1 @@ -15392,12 +15487,12 @@ packages: optional: true dev: false - /ws@8.8.1: - resolution: {integrity: sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==} + /ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: '>=5.0.2' peerDependenciesMeta: bufferutil: optional: true From 870bff9059256828a1f40af2344842540dbb2c9a Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Wed, 2 Oct 2024 14:01:46 +0000 Subject: [PATCH 071/111] Assistant: Fixed text field autofocus --- ui/packages/platform/src/pages/Bot/Command/Command.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/platform/src/pages/Bot/Command/Command.tsx b/ui/packages/platform/src/pages/Bot/Command/Command.tsx index 35f54f77..fb4aff53 100644 --- a/ui/packages/platform/src/pages/Bot/Command/Command.tsx +++ b/ui/packages/platform/src/pages/Bot/Command/Command.tsx @@ -194,7 +194,7 @@ export const Command = React.memo((props: Props) => { if (!inputRef.current) return if (window.innerWidth > theme.breakpoints.values.md) inputRef.current.focus() if (!location.state?.skipReloading) setValue('') - }, [threadId]); + }, [threadId, loading]); return (
From c2aad56b9393f6cf8420d436824041b3fbb369ad Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Wed, 2 Oct 2024 19:13:01 +0000 Subject: [PATCH 072/111] gpt4o-mini by default --- ui/packages/platform/src/pages/Bot/hooks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/platform/src/pages/Bot/hooks.tsx b/ui/packages/platform/src/pages/Bot/hooks.tsx index 5c388ac7..5651771d 100644 --- a/ui/packages/platform/src/pages/Bot/hooks.tsx +++ b/ui/packages/platform/src/pages/Bot/hooks.tsx @@ -20,7 +20,7 @@ import { getDebugMessages } from "../../api/bot/getDebugMessages"; const WS_URL = process.env.REACT_APP_WS_URL || ''; -const DEFAULT_MODEL_NAME = 'gemini-1.5-pro' +const DEFAULT_MODEL_NAME = 'gpt-4o-mini' type ErrorType = { code?: number; From 4a096071f79c6e318ab72a54eff2188a8b26b0e5 Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Thu, 3 Oct 2024 17:57:40 +0000 Subject: [PATCH 073/111] Add PostgreSQL 17 (release) --- engine/.gitlab-ci.yml | 2 +- .../shared/pages/Configuration/utils/index.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/engine/.gitlab-ci.yml b/engine/.gitlab-ci.yml index 47ec2276..7b8f074f 100644 --- a/engine/.gitlab-ci.yml +++ b/engine/.gitlab-ci.yml @@ -472,7 +472,7 @@ bash-test-16: bash-test-17: <<: *bash_test variables: - POSTGRES_VERSION: 17rc1 + POSTGRES_VERSION: 17 integration-test: services: diff --git a/ui/packages/shared/pages/Configuration/utils/index.ts b/ui/packages/shared/pages/Configuration/utils/index.ts index 216de9c9..e4ee4d5c 100644 --- a/ui/packages/shared/pages/Configuration/utils/index.ts +++ b/ui/packages/shared/pages/Configuration/utils/index.ts @@ -7,15 +7,15 @@ const seContainerRegistry = 'se-images' const genericImagePrefix = 'postgresai/extended-postgres' // since some tags are rc, we need to specify the exact tags to use const dockerImagesConfig = { - '9.6': ['0.5.0', '0.4.6', '0.4.5'], - '10': ['0.5.0', '0.4.6', '0.4.5'], - '11': ['0.5.0', '0.4.6', '0.4.5'], - '12': ['0.5.0', '0.4.6', '0.4.5'], - '13': ['0.5.0', '0.4.6', '0.4.5'], - '14': ['0.5.0', '0.4.6', '0.4.5'], - '15': ['0.5.0', '0.4.6', '0.4.5'], - '16': ['0.5.0', '0.4.6', '0.4.5'], - '17rc1': ['0.5.0'], + '9.6': ['0.5.1', '0.4.6', '0.4.5'], + '10': ['0.5.1', '0.4.6', '0.4.5'], + '11': ['0.5.1', '0.4.6', '0.4.5'], + '12': ['0.5.1', '0.4.6', '0.4.5'], + '13': ['0.5.1', '0.4.6', '0.4.5'], + '14': ['0.5.1', '0.4.6', '0.4.5'], + '15': ['0.5.1', '0.4.6', '0.4.5'], + '16': ['0.5.1', '0.4.6', '0.4.5'], + '17': ['0.5.1'], } export type FormValuesKey = keyof FormValues From 5012753a5bd601e5684b19f402f0ae48f07d4c19 Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Fri, 11 Oct 2024 16:12:51 +0000 Subject: [PATCH 074/111] Add image tag v0.5.2 --- .../shared/pages/Configuration/utils/index.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ui/packages/shared/pages/Configuration/utils/index.ts b/ui/packages/shared/pages/Configuration/utils/index.ts index e4ee4d5c..22076810 100644 --- a/ui/packages/shared/pages/Configuration/utils/index.ts +++ b/ui/packages/shared/pages/Configuration/utils/index.ts @@ -7,15 +7,15 @@ const seContainerRegistry = 'se-images' const genericImagePrefix = 'postgresai/extended-postgres' // since some tags are rc, we need to specify the exact tags to use const dockerImagesConfig = { - '9.6': ['0.5.1', '0.4.6', '0.4.5'], - '10': ['0.5.1', '0.4.6', '0.4.5'], - '11': ['0.5.1', '0.4.6', '0.4.5'], - '12': ['0.5.1', '0.4.6', '0.4.5'], - '13': ['0.5.1', '0.4.6', '0.4.5'], - '14': ['0.5.1', '0.4.6', '0.4.5'], - '15': ['0.5.1', '0.4.6', '0.4.5'], - '16': ['0.5.1', '0.4.6', '0.4.5'], - '17': ['0.5.1'], + '9.6': ['0.5.2', '0.5.1', '0.4.6'], + '10': ['0.5.2', '0.5.1', '0.4.6'], + '11': ['0.5.2', '0.5.1', '0.4.6'], + '12': ['0.5.2', '0.5.1', '0.4.6'], + '13': ['0.5.2', '0.5.1', '0.4.6'], + '14': ['0.5.2', '0.5.1', '0.4.6'], + '15': ['0.5.2', '0.5.1', '0.4.6'], + '16': ['0.5.2', '0.5.1', '0.4.6'], + '17': ['0.5.2', '0.5.1'], } export type FormValuesKey = keyof FormValues From fa25e36267258a572cc483cbeda94e3df7b818e9 Mon Sep 17 00:00:00 2001 From: Nikolay Samokhvalov Date: Thu, 31 Oct 2024 14:19:12 +0000 Subject: [PATCH 075/111] fix contributors link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b5bbc02..03f77d9e 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ Reach out to the Postgres.ai team if you want a trial or commercial license that Many thanks to our amazing contributors! - + From ca560972663a6b131dc6977771aeef667a65351a Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Wed, 6 Nov 2024 18:43:17 +0000 Subject: [PATCH 076/111] Platform (UI): Update dle-se-ansible image tag to v1.1 --- .../platform/src/components/DbLabInstanceForm/utils/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx index 7dcafee6..5821d379 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx @@ -10,7 +10,7 @@ export const DEBUG_API_SERVER = 'https://v2.postgres.ai/api/general' export const availableTags = ['3.5.0', '3.4.0', '4.0.0-alpha.6'] -export const sePackageTag = 'v1.0' +export const sePackageTag = 'v1.1' export const dockerRunCommand = (provider: string) => { switch (provider) { From 07cf25c295ff624be50264028fcb7efba70e3651 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 7 Nov 2024 13:24:22 +0000 Subject: [PATCH 077/111] Assistant: Disable HTML rendering in user messages --- .../platform/src/pages/Bot/Messages/Message/Message.tsx | 3 +-- ui/packages/platform/src/pages/Bot/utils.ts | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx index 40d63dc1..5aa894c4 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx @@ -275,7 +275,6 @@ export const Message = React.memo((props: MessageProps) => { setDebugVisible(prevState => !prevState) } - const renderers = useMemo(() => ({ p: ({ node, ...props }) =>
, img: ({ node, ...props }) => , @@ -368,7 +367,7 @@ export const Message = React.memo((props: MessageProps) => { : { From 52076fac5931a88c99650eb305870510fcca65f8 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 7 Nov 2024 13:24:32 +0000 Subject: [PATCH 078/111] Assistant: Hide footer on assistant page for mobile devices --- .../components/ContentLayout/Footer/index.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx b/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx index d1671c40..8ef280ca 100644 --- a/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx +++ b/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx @@ -5,10 +5,13 @@ *-------------------------------------------------------------------------- */ -import { makeStyles } from '@material-ui/core' +import { makeStyles, useMediaQuery } from '@material-ui/core' import { GatewayLink } from '@postgres.ai/shared/components/GatewayLink' +import { useMemo } from 'react' +import { useLocation } from 'react-router-dom' import settings from 'utils/settings' +import cn from "classnames"; const useStyles = makeStyles( (theme) => ({ @@ -24,6 +27,9 @@ const useStyles = makeStyles( flexDirection: 'column', }, }, + hidden: { + display: 'none' + }, footerCopyrightItem: { marginRight: 50, [theme.breakpoints.down('sm')]: { @@ -66,9 +72,15 @@ const useStyles = makeStyles( export const Footer = () => { const classes = useStyles() + const location = useLocation(); + const isMobile = useMediaQuery('(max-width:480px)'); + + const isAssistantPage = useMemo(() => { + return /^\/[^\/]+\/assistant(\/[^\/]+)?\/?$/.test(location.pathname); + }, [location.pathname]); return ( -
) } From 02d7ac2884da84605c447be49bafc5757de46882 Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Thu, 5 Dec 2024 17:16:24 +0000 Subject: [PATCH 086/111] Update dle-se-ansible image tag to v1.3 --- .../platform/src/components/DbLabInstanceForm/utils/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx index 5821d379..18e41213 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx @@ -10,7 +10,7 @@ export const DEBUG_API_SERVER = 'https://v2.postgres.ai/api/general' export const availableTags = ['3.5.0', '3.4.0', '4.0.0-alpha.6'] -export const sePackageTag = 'v1.1' +export const sePackageTag = 'v1.3' export const dockerRunCommand = (provider: string) => { switch (provider) { From ec72050cce25373d2150999d1e7ab429fd9ac260 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Mon, 9 Dec 2024 16:09:16 +0000 Subject: [PATCH 087/111] feat (UI): Display warning when a plan returns zero rows --- .../JoeSessionCommandWrapper.jsx | 3 +++ .../src/pages/JoeSessionCommand/index.js | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/ui/packages/platform/src/pages/JoeSessionCommand/JoeSessionCommandWrapper.jsx b/ui/packages/platform/src/pages/JoeSessionCommand/JoeSessionCommandWrapper.jsx index 364e41b1..eeced9de 100644 --- a/ui/packages/platform/src/pages/JoeSessionCommand/JoeSessionCommandWrapper.jsx +++ b/ui/packages/platform/src/pages/JoeSessionCommand/JoeSessionCommandWrapper.jsx @@ -38,6 +38,9 @@ export const JoeSessionCommandWrapper = (props) => { bottomSpace: { ...styles.bottomSpace, }, + warningContainer: { + marginTop: theme.spacing(2) + } }), { index: 1 }, ) diff --git a/ui/packages/platform/src/pages/JoeSessionCommand/index.js b/ui/packages/platform/src/pages/JoeSessionCommand/index.js index d416cc97..dc2b364f 100644 --- a/ui/packages/platform/src/pages/JoeSessionCommand/index.js +++ b/ui/packages/platform/src/pages/JoeSessionCommand/index.js @@ -35,6 +35,7 @@ import Permissions from 'utils/permissions'; import format from 'utils/format'; import { TabPanel } from './TabPanel'; +import Alert from "@mui/material/Alert"; const hashLinkVisualizePrefix = 'visualize-'; @@ -139,6 +140,19 @@ class JoeSessionCommand extends Component { return !!data && data.command === 'explain'; }; + planHasNoRows = () => { + if (!this.isExplain()) return false; + const data = this.getCommandData(); + const planExecJson = data && data.planExecJson; + if (!planExecJson) return false; + + const planExecJsonParsed = JSON.parse(planExecJson); + if (!Array.isArray(planExecJsonParsed) || planExecJsonParsed.length === 0) return false; + + const plan = planExecJsonParsed[0] && planExecJsonParsed[0]["Plan"]; + return plan && plan["Actual Rows"] === 0; + } + showExternalVisualization = (type) => { const data = this.getCommandData(); @@ -351,6 +365,11 @@ class JoeSessionCommand extends Component {
} + {this.planHasNoRows() &&
+ Query returned 0 rows. This may not reflect production performance or use the same query plan. If you expect results, try adjusting parameters (e.g., different ID values). + +
} +

Author:

From 8d61cd37033ceb7b9b7beea9396bffe47ebfdcac Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Wed, 11 Dec 2024 18:04:46 +0000 Subject: [PATCH 088/111] Bot UI: LLM selector - display comment field --- .../platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx | 2 +- ui/packages/platform/src/types/api/entities/bot.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/packages/platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx b/ui/packages/platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx index 0ff0dac2..d34957dc 100644 --- a/ui/packages/platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx +++ b/ui/packages/platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx @@ -356,7 +356,7 @@ export const SettingsDialog = (props: PublicChatDialogProps) => { key={`${model.vendor}/${model.name}`} value={`${model.vendor}/${model.name}`} control={} - label={model.name} + label={`${model.name} ${model.comment ? model.comment : ''}`} /> ) } diff --git a/ui/packages/platform/src/types/api/entities/bot.ts b/ui/packages/platform/src/types/api/entities/bot.ts index 9fe303b2..773859a6 100644 --- a/ui/packages/platform/src/types/api/entities/bot.ts +++ b/ui/packages/platform/src/types/api/entities/bot.ts @@ -30,6 +30,7 @@ export type BotMessageWithDebugInfo = BotMessage & { } export type AiModel = { + comment: string; name: string; vendor: string; isThirdParty: boolean; From 78d157dab916beb236026a1e98af67f77856799a Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 20 Dec 2024 02:34:32 +0000 Subject: [PATCH 089/111] Assistant UI: Fix link to consulting --- ui/packages/platform/src/pages/Bot/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/platform/src/pages/Bot/index.tsx b/ui/packages/platform/src/pages/Bot/index.tsx index 12259bab..db6a6269 100644 --- a/ui/packages/platform/src/pages/Bot/index.tsx +++ b/ui/packages/platform/src/pages/Bot/index.tsx @@ -184,7 +184,7 @@ export const BotPage = (props: BotPageProps) => { } if (!isSubscriber) { - return <>AI can make mistakes❗️ All chats here are currently public. Connect DBLab SE or become a consulting client to enable private conversations; + return <>AI can make mistakes❗️ All chats here are currently public. Connect DBLab SE or become a consulting client to enable private conversations; } if (isSubscriber && chatVisibility === 'public') { From cc1fdc0d5c46293489051292d1d3aeb01a4e2ddf Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 20 Dec 2024 02:35:08 +0000 Subject: [PATCH 090/111] Assistant UI: Switch to bot_llm_models API endpoint --- .../platform/src/api/bot/getAiModels.ts | 19 +++++++++++++------ ui/packages/platform/src/pages/Bot/hooks.tsx | 6 +++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/ui/packages/platform/src/api/bot/getAiModels.ts b/ui/packages/platform/src/api/bot/getAiModels.ts index 2e4e2056..b8e1afb0 100644 --- a/ui/packages/platform/src/api/bot/getAiModels.ts +++ b/ui/packages/platform/src/api/bot/getAiModels.ts @@ -1,21 +1,28 @@ import {request} from "../../helpers/request"; import { AiModel } from "../../types/api/entities/bot"; -export const getAiModels = async (): Promise<{ response: AiModel[] | null; error: Response | null }> => { +export const getAiModels = async (orgId?: number): Promise<{ response: AiModel[] | null; error: Response | null }> => { const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; - + const body = { + org_id: orgId + } try { - const response = await request(`${apiServer}/llm_models`, { - method: 'GET', + const response = await request(`${apiServer}/rpc/bot_llm_models`, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Accept': 'application/vnd.pgrst.object+json', + 'Prefer': 'return=representation', + } }); if (!response.ok) { return { response: null, error: response }; } - const responseData: AiModel[] = await response.json(); + const responseData: { bot_llm_models: AiModel[] | null } = await response.json(); - return { response: responseData, error: null }; + return { response: responseData?.bot_llm_models, error: null }; } catch (error) { return { response: null, error: error as Response }; diff --git a/ui/packages/platform/src/pages/Bot/hooks.tsx b/ui/packages/platform/src/pages/Bot/hooks.tsx index 0eecc1a4..840b3785 100644 --- a/ui/packages/platform/src/pages/Bot/hooks.tsx +++ b/ui/packages/platform/src/pages/Bot/hooks.tsx @@ -89,7 +89,7 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => aiModel, setAiModel, loading: aiModelsLoading - } = useAiModelsList(); + } = useAiModelsList(orgId); let location = useLocation<{skipReloading?: boolean}>(); const { @@ -567,7 +567,7 @@ type UseAiModelsList = { setAiModel: (model: AiModel) => void } -export const useAiModelsList = (): UseAiModelsList => { +export const useAiModelsList = (orgId?: number): UseAiModelsList => { const [llmModels, setLLMModels] = useState(null); const [error, setError] = useState(null); const [userModel, setUserModel] = useState(null); @@ -577,7 +577,7 @@ export const useAiModelsList = (): UseAiModelsList => { let models = null; setLoading(true); try { - const { response } = await getAiModels(); + const { response } = await getAiModels(orgId); setLLMModels(response); const currentModel = window.localStorage.getItem('bot.ai_model'); const parsedModel: AiModel = currentModel ? JSON.parse(currentModel) : null; From 76632e1641ade252a282f55a10ad8ecb58db8e7a Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 20 Dec 2024 03:32:22 +0000 Subject: [PATCH 091/111] hotfix: Assistant UI llm models list access --- ui/packages/platform/src/api/bot/getAiModels.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/packages/platform/src/api/bot/getAiModels.ts b/ui/packages/platform/src/api/bot/getAiModels.ts index b8e1afb0..eba0a0c0 100644 --- a/ui/packages/platform/src/api/bot/getAiModels.ts +++ b/ui/packages/platform/src/api/bot/getAiModels.ts @@ -20,9 +20,9 @@ export const getAiModels = async (orgId?: number): Promise<{ response: AiModel[] return { response: null, error: response }; } - const responseData: { bot_llm_models: AiModel[] | null } = await response.json(); + const responseData: AiModel[] | null = await response.json(); - return { response: responseData?.bot_llm_models, error: null }; + return { response: responseData, error: null }; } catch (error) { return { response: null, error: error as Response }; From a21b18b06a9dbc8c5ca2b63674ec39d2246c8823 Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Fri, 20 Dec 2024 17:39:33 +0000 Subject: [PATCH 092/111] Update demo url to demo.dblab.dev --- README.md | 2 +- engine/api/swagger-spec/dblab_server_swagger.yaml | 2 +- ui/packages/ce/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 03f77d9e..6df7f229 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Try it yourself right now: - Visit [Postgres.ai Console](https://console.postgres.ai/), set up your first organization and provision a DBLab Standard Edition (DBLab SE) to any cloud or on-prem - [Pricing](https://postgres.ai/pricing) (starting at $62/month) - [Doc: How to install DBLab SE](https://postgres.ai/docs/how-to-guides/administration/install-dle-from-postgres-ai) -- Demo: https://demo.aws.postgres.ai (use the token `demo-token` to access) +- Demo: https://demo.dblab.dev (use the token `demo-token` to access) - Looking for a free version? Install DBLab Community Edition by [following this tutorial](https://postgres.ai/docs/tutorials/database-lab-tutorial) ## How it works diff --git a/engine/api/swagger-spec/dblab_server_swagger.yaml b/engine/api/swagger-spec/dblab_server_swagger.yaml index cf93cead..5242ed8a 100644 --- a/engine/api/swagger-spec/dblab_server_swagger.yaml +++ b/engine/api/swagger-spec/dblab_server_swagger.yaml @@ -21,7 +21,7 @@ externalDocs: url: https://gitlab.com/postgres-ai/docs/tree/master/docs/database-lab servers: - - url: "https://demo.aws.postgres.ai/api" + - url: "https://demo.dblab.dev/api" description: "DBLab 3.x demo server; token: 'demo-token'" x-examples: Verification-Token: "demo-token" diff --git a/ui/packages/ce/package.json b/ui/packages/ce/package.json index 232deab9..f2608d22 100644 --- a/ui/packages/ce/package.json +++ b/ui/packages/ce/package.json @@ -90,5 +90,5 @@ "stylelint-config-standard-scss": "^2.0.1", "stylelint-prettier": "^2.0.0" }, - "proxy": "https://demo.aws.postgres.ai:446" + "proxy": "https://demo.dblab.dev:446" } From 7cb78537b01820f8ec2d3d4d5fe05ee6d47eef26 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Mon, 23 Dec 2024 16:51:48 +0000 Subject: [PATCH 093/111] feat (UI): Collapsible menu items --- .../src/components/IndexPage/IndexPage.tsx | 642 ++++++++++-------- .../components/IndexPage/IndexPageWrapper.tsx | 25 +- 2 files changed, 378 insertions(+), 289 deletions(-) diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index 18fb50c0..c5a20e98 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -5,7 +5,7 @@ *-------------------------------------------------------------------------- */ -import { Component } from 'react' +import React, { Component, useState } from 'react' import { Switch, Route, NavLink, Redirect, useRouteMatch } from 'react-router-dom' import { AppBar, @@ -16,13 +16,15 @@ import { ListItem, List, Drawer, + Collapse, + Box, + ListItemText } from '@material-ui/core' import qs from 'qs' import { icons } from '@postgres.ai/shared/styles/icons' import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner' import { GatewayLink } from '@postgres.ai/shared/components/GatewayLink' -import { Box } from '@mui/material' import { OrganizationWrapperProps, OrganizationMenuProps, @@ -85,6 +87,7 @@ import { PostgresClusterInstallWrapper } from 'components/PostgresClusterInstall import { PostgresClustersWrapper } from 'components/PostgresClusters/PostgresClustersWrapper' import cn from "classnames"; import { BotSettingsFormWrapper } from "../BotSettingsForm/BotSettingsFormWrapper"; +import { ExpandLess, ExpandMore } from "@material-ui/icons"; interface IndexPageWithStylesProps extends IndexPageProps { @@ -291,7 +294,21 @@ function ProjectWrapper(parentProps: Omit) { } function OrganizationMenu(parentProps: OrganizationMenuProps) { - const isDemoOrg = useRouteMatch(`/${settings.demoOrgAlias}`) + const [activeMenuItems, setActiveMenuItems] = useState>(new Set()); + + const handleOpenMenuItem = (e: React.MouseEvent, value: string) => { + e.stopPropagation() + setActiveMenuItems((prev) => { + const newSet = new Set(prev); + if (newSet.has(value)) { + newSet.delete(value); + } else { + newSet.add(value); + } + return newSet; + }); + }; + if ( parentProps.env && parentProps.env.data && @@ -315,7 +332,7 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { } return ( -

+
Organization @@ -333,329 +350,380 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) {
- - - - - - {icons.dashboardIcon} - - Dashboard - - - - - - - {icons.aiBotIcon} - - AI AssistantNEW - - - - - - {icons.databaseLabIcon} - - Database Lab - - - - - Instances - - - - - Observed sessions - - - - - - {icons.sqlOptimizationIcon} - - SQL Optimization - - - - - Ask Joe - - - - - History - - - {false && ( +
+ - Plan visualization + + {icons.dashboardIcon} + + Dashboard - )} - - {/* - - - {icons.postgresSQLIcon} - - Postgres - - - - - Clusters - - */} - - - - - {icons.checkupIcon} - - Checkup - - - - - Reports - - - - - - {icons.settingsIcon} - - Settings - - - {orgPermissions && orgPermissions.settingsOrganizationUpdate && ( - General + + {icons.aiBotIcon} + + AI AssistantNEW - )} - {orgPermissions && orgPermissions.settingsOrganizationUpdate && ( handleOpenMenuItem(e, 'dblab')} > - - AI Assistant - + + {icons.databaseLabIcon} + + Database Lab + {activeMenuItems.has('dblab') + ? + : } + - )} - - - Members - - - - - Access tokens - - - {orgData !== null && Permissions.isAdmin(orgData) && ( + + + + Instances + + + + + Observed sessions + + + + handleOpenMenuItem(e, 'sqlOptimization')} + > + + + {icons.sqlOptimizationIcon} + + SQL Optimization + {activeMenuItems.has('sqlOptimization') + ? + : } + + + + + + + Ask Joe + + + + + History + + + {false && ( + + + Plan visualization + + + )} + + + {/* - Billing + + {icons.postgresSQLIcon} + + Postgres - )} - {orgPermissions && orgPermissions.auditLogView && ( - Audit + Clusters + */} + + handleOpenMenuItem(e, 'checkup')} + > + + + {icons.checkupIcon} + + Checkup + {activeMenuItems.has('checkup') + ? + : } + - )} - + + + + + Reports + + + + + handleOpenMenuItem(e, 'settings')} + > + + + {icons.settingsIcon} + + Settings + {activeMenuItems.has('settings') + ? + : } + + + + + {orgPermissions && orgPermissions.settingsOrganizationUpdate && ( + + + General + + + )} + {orgPermissions && orgPermissions.settingsOrganizationUpdate && ( + + + AI Assistant + + + )} + + + Members + + + + + Access tokens + + + {orgData !== null && Permissions.isAdmin(orgData) && ( + + + Billing + + + )} + {orgPermissions && orgPermissions.auditLogView && ( + + + Audit + + + )} + + + +
) } diff --git a/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx b/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx index 156abe51..088cb735 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx @@ -9,7 +9,7 @@ export interface IndexPageProps Omit {} export const IndexPageWrapper = (props: IndexPageProps) => { - const drawerWidth = 185 + const drawerWidth = 190 const useStyles = makeStyles( (theme) => ({ @@ -205,6 +205,9 @@ export const IndexPageWrapper = (props: IndexPageProps) => { padding: '0px', marginTop: '10px', }, + menuSectionHeaderCollapsible: { + marginTop: 0 + }, bottomFixedMenuItem: { fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', fontStyle: 'normal', @@ -236,15 +239,28 @@ export const IndexPageWrapper = (props: IndexPageProps) => { paddingLeft: '15px', color: '#000000', }, + menuSectionHeaderLinkText: { + display: 'inline-flex', + alignItems: 'center', + fontWeight: 'bold' + }, menuSingleSectionHeaderActiveLink: { backgroundColor: colors.consoleStroke, }, menuPointer: { height: '100%', }, + menuContainer: { + height: 'calc(100% - 90px)', + display: 'flex', + flexDirection: 'column' + }, + navMenuContainer: { + flex: 1, + overflowY: 'auto' + }, navMenu: { padding: '0px', - height: 'calc(100% - 160px)', overflowY: 'auto', display: 'flex', @@ -253,6 +269,11 @@ export const IndexPageWrapper = (props: IndexPageProps) => { menuSectionHeaderIcon: { marginRight: '13px', }, + menuSectionHeaderExpandIcon: { + marginLeft: 4, + width: 16, + height: 16 + }, menuItem: { fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', fontStyle: 'normal', From 77c3404415a451493e01d97a63f11bb324294123 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 26 Dec 2024 16:32:36 +0000 Subject: [PATCH 094/111] feat(ui): Integration of audit logs with SIEM systems --- ui/cspell.json | 3 +- ui/packages/platform/src/actions/actions.js | 114 ++++- ui/packages/platform/src/api/api.js | 76 ++++ .../platform/src/components/Audit/Audit.tsx | 12 +- .../AuditSettingsForm/AuditSettingsForm.tsx | 423 ++++++++++++++++++ .../AuditSettingsFormWrapper.tsx | 32 ++ .../src/components/IndexPage/IndexPage.tsx | 44 +- .../components/IndexPage/IndexPageWrapper.tsx | 9 +- .../SIEMIntegrationForm.tsx | 181 ++++++++ ui/packages/platform/src/stores/store.js | 90 +++- ui/packages/shared/styles/icons.tsx | 8 + 11 files changed, 974 insertions(+), 18 deletions(-) create mode 100644 ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx create mode 100644 ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsFormWrapper.tsx create mode 100644 ui/packages/platform/src/components/SIEMIntegrationForm/SIEMIntegrationForm.tsx diff --git a/ui/cspell.json b/ui/cspell.json index 62305d93..6cc3ab15 100644 --- a/ui/cspell.json +++ b/ui/cspell.json @@ -201,6 +201,7 @@ "sparql", "SPARQL", "subtransactions", - "mbox" + "mbox", + "SIEM" ] } diff --git a/ui/packages/platform/src/actions/actions.js b/ui/packages/platform/src/actions/actions.js index 507200e2..f16a8242 100644 --- a/ui/packages/platform/src/actions/actions.js +++ b/ui/packages/platform/src/actions/actions.js @@ -54,6 +54,7 @@ const Actions = Reflux.createActions([{ updateOrg: ASYNC_ACTION, createOrg: ASYNC_ACTION, updateAiBotSettings: ASYNC_ACTION, + updateAuditSettings: ASYNC_ACTION, inviteUser: ASYNC_ACTION, useDemoData: ASYNC_ACTION, setReportsProject: {}, @@ -114,7 +115,9 @@ const Actions = Reflux.createActions([{ downloadDblabSessionArtifact: ASYNC_ACTION, sendUserCode: ASYNC_ACTION, confirmUserEmail: ASYNC_ACTION, - confirmTosAgreement: ASYNC_ACTION + confirmTosAgreement: ASYNC_ACTION, + testSiemServiceConnection: ASYNC_ACTION, + getAuditEvents: ASYNC_ACTION }]); function timeoutPromise(ms, promise) { @@ -654,6 +657,42 @@ Actions.updateAiBotSettings.listen(function (token, orgId, orgData) { }); }); +Actions.updateAuditSettings.listen(function (token, orgId, orgData) { + let action = this; + + if (!api) { + settings.init(function () { + api = new Api(settings); + }); + } + + action.progressed({ orgId } + orgData); + timeoutPromise(REQUEST_TIMEOUT, api.updateAuditSettings(token, orgId, orgData)) + + .then(result => { + result.json() + .then(json => { + if (json) { + action.completed(json); + } else { + action.failed(new Error('wrong_reply')); + } + }) + .catch(err => { + console.error(err); + action.failed(new Error('wrong_reply')); + }); + }) + .catch(err => { + console.error(err); + if (err && err.message && err.message === 'timeout') { + action.failed(new Error('failed_fetch')); + } else { + action.failed(new Error('wrong_reply')); + } + }); +}); + Actions.createOrg.listen(function (token, orgData) { let action = this; @@ -1571,4 +1610,77 @@ Actions.confirmTosAgreement.listen(function (token) { ); }); + +Actions.testSiemServiceConnection.listen(function (token, data) { + let action = this; + + if (!api) { + settings.init(function () { + api = new Api(settings); + }); + } + + action.progressed(data); + timeoutPromise(REQUEST_TIMEOUT, api.testSiemServiceConnection(token, data)) + + .then(result => { + result.json() + .then(json => { + if (json) { + action.completed(json); + } else { + action.failed(new Error('wrong_reply')); + } + }) + .catch(err => { + console.error(err); + action.failed(new Error('wrong_reply')); + }); + }) + .catch(err => { + console.error(err); + if (err && err.message && err.message === 'timeout') { + action.failed(new Error('failed_fetch')); + } else { + action.failed(new Error('wrong_reply')); + } + }); +}); + +Actions.getAuditEvents.listen(function (token) { + let action = this; + + if (!api) { + settings.init(function () { + api = new Api(settings); + }); + } + + action.progressed(); + + timeoutPromise(REQUEST_TIMEOUT, api.getAuditEvents(token)) + .then(result => { + result.json() + .then(json => { + if (json) { + action.completed({ data: json }); + } else { + action.failed(new Error('wrong_reply')); + } + }) + .catch(err => { + console.error(err); + action.failed(new Error('wrong_reply')); + }); + }) + .catch(err => { + console.error(err); + if (err && err.message && err.message === 'timeout') { + action.failed(new Error('failed_fetch')); + } else { + action.failed(new Error('wrong_reply')); + } + }); +}); + export default Actions; diff --git a/ui/packages/platform/src/api/api.js b/ui/packages/platform/src/api/api.js index 308864b0..14d2d033 100644 --- a/ui/packages/platform/src/api/api.js +++ b/ui/packages/platform/src/api/api.js @@ -467,6 +467,71 @@ class Api { }); } + updateAuditSettings(token, orgId, orgData) { + let params = {}; + let headers = { + Authorization: 'Bearer ' + token, + prefer: 'return=representation' + }; + + if (typeof orgData.enableSiemIntegration !== 'undefined') { + params.siem_integration_enabled = orgData.enableSiemIntegration; + } + + if (typeof orgData.urlSchema !== 'undefined') { + params.siem_integration_url = orgData.urlSchema; + } + + if (typeof orgData.auditEvents !== "undefined") { + params.audit_events_to_log = orgData.auditEvents.map((item) => item.event_name) + } + + if (typeof orgData.headers !== 'undefined' && Array.isArray(orgData.headers)) { + orgData.headers = orgData.headers.filter(item => item.key && item.value); + if (Object.keys(orgData.headers).length > 0) { + params.siem_integration_request_headers = orgData.headers.reduce((acc, item) => { + acc[item.key] = item.value; + return acc; + }, {}); + } else { + params.siem_integration_request_headers = null + } + } + + return this.patch(`${this.apiServer}/orgs?id=eq.` + orgId, params, { + headers: headers + }); + } + + testSiemServiceConnection(token, data) { + let params = {}; + let headers = { + Accept: 'application/vnd.pgrst.object+json', + Authorization: 'Bearer ' + token, + prefer: 'return=representation' + }; + + if (typeof data.urlSchema !== 'undefined') { + params.api_url = data.urlSchema; + } + + if (typeof data.headers !== 'undefined' && Array.isArray(data.headers)) { + data.headers = data.headers.filter(item => item.key && item.value); + if (Object.keys(data.headers).length > 0) { + params.http_headers_extra = data.headers.reduce((acc, item) => { + acc[item.key] = item.value; + return acc; + }, {}); + } else { + params.http_headers_extra = null + } + } + + return this.post(`${this.apiServer}/rpc/test_siem_connection`, params, { + headers: headers + }); + } + inviteUser(token, orgId, email) { let headers = { Authorization: 'Bearer ' + token @@ -992,6 +1057,17 @@ class Api { { headers } ); } + + getAuditEvents(token) { + let params = {}; + let headers = { + Authorization: 'Bearer ' + token + }; + + return this.get(`${this.apiServer}/audit_events`, params, { + headers: headers + }); + } } export default Api; diff --git a/ui/packages/platform/src/components/Audit/Audit.tsx b/ui/packages/platform/src/components/Audit/Audit.tsx index 6704de25..7e6b337d 100644 --- a/ui/packages/platform/src/components/Audit/Audit.tsx +++ b/ui/packages/platform/src/components/Audit/Audit.tsx @@ -50,7 +50,7 @@ export interface AuditLogData { action: string actor: string action_data: { - processed_rows_count: number + processed_row_count: number } created_at: string table_name: string @@ -155,11 +155,11 @@ class Audit extends Component { actorSrc = ' (changed directly in database) ' } - if (r.action_data && r.action_data.processed_rows_count) { + if (r.action_data && r.action_data.processed_row_count) { rows = - r.action_data.processed_rows_count + + r.action_data.processed_row_count + ' ' + - (r.action_data.processed_rows_count > 1 ? 'rows' : 'row') + (r.action_data.processed_row_count > 1 ? 'rows' : 'row') } switch (r.action) { @@ -197,8 +197,8 @@ class Audit extends Component { ? r.data_before?.length : r.data_after?.length const objCount = - r.action_data && r.action_data.processed_rows_count - ? r.action_data.processed_rows_count + r.action_data && r.action_data.processed_row_count + ? r.action_data.processed_row_count : null if (displayedCount && (objCount as number) > displayedCount) { diff --git a/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx new file mode 100644 index 00000000..a952f1bd --- /dev/null +++ b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx @@ -0,0 +1,423 @@ +/*-------------------------------------------------------------------------- + * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai + * All Rights Reserved. Proprietary and confidential. + * Unauthorized copying of this file, via any medium is strictly prohibited + *-------------------------------------------------------------------------- + */ + +import React, { useEffect, useMemo, useState } from 'react' +import { Link } from '@postgres.ai/shared/components/Link2' +import { + Grid, + Button, + FormControl, + FormControlLabel, + makeStyles, + Typography +} from '@material-ui/core' +import * as Yup from 'yup'; +import Store from '../../stores/store' +import Actions from '../../actions/actions' +import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' +import ConsolePageTitle from '../ConsolePageTitle' +import { AuditSettingsFormProps } from './AuditSettingsFormWrapper' +import { styles } from "@postgres.ai/shared/styles/styles"; +import { PageSpinner } from "@postgres.ai/shared/components/PageSpinner"; +import { WarningWrapper } from "../Warning/WarningWrapper"; +import { messages } from "../../assets/messages"; +import { ExternalIcon } from "@postgres.ai/shared/icons/External"; +import { useFormik } from "formik"; +import Checkbox from '@mui/material/Checkbox/Checkbox' +import { SIEMIntegrationForm } from "../SIEMIntegrationForm/SIEMIntegrationForm"; + +type AuditSettingState = { + data: { + auth: { + token: string | null + } | null + orgProfile: { + isUpdating: boolean + error: boolean + updateError: boolean + errorMessage: string | undefined + errorCode: number | undefined + updateErrorMessage: string | null + isProcessing: boolean + orgId: number | null + updateErrorFields: string[] + data: { + siem_integration_enabled: SiemSettings["enableSiemIntegration"] + siem_integration_url: SiemSettings["urlSchema"] + siem_integration_request_headers: SiemSettings["headers"] + audit_events_to_log: string[] + } + } | null + auditEvents: { + isProcessing: boolean + data: { + id: number + event_name: string + label: string + }[] | null + } | null + } | null +} + +interface SiemSettings { + enableSiemIntegration: boolean; + urlSchema?: string; + headers: { key: string; value: string }[]; + auditEvents: EventsToLog[]; +} + +interface EventsToLog { + id: number; + event_name: string; + label: string; +} + +export interface FormValues { + siemSettings: SiemSettings; +} + +const useStyles = makeStyles( + { + container: { + ...(styles.root as Object), + display: 'flex', + 'flex-wrap': 'wrap', + 'min-height': 0, + '&:not(:first-child)': { + 'margin-top': '20px', + }, + }, + textField: { + ...styles.inputField, + }, + instructionsField: { + ...styles.inputField, + }, + selectField: { + marginTop: 4, + + }, + label: { + color: '#000!important', + fontWeight: 'bold', + }, + updateButtonContainer: { + marginTop: 20, + textAlign: 'left', + }, + unlockNote: { + marginTop: 8, + '& ol': { + paddingLeft: 24, + marginTop: 6, + marginBottom: 0 + } + }, + externalIcon: { + width: 14, + height: 14, + marginLeft: 4, + transform: 'translateY(2px)', + }, + testConnectionButton: { + marginRight: 16 + }, + eventRow: { + display: 'flex', + alignItems: 'center', + marginBottom: '10px', + }, + }, + { index: 1 }, +) + +const validationSchema = Yup.object({ + siemSettings: Yup.object({ + urlSchema: Yup.string() + .url('Invalid URL format') // Validates that the input is a valid URL + .required('URL is required'), // Field is mandatory + headers: Yup.array().of( + Yup.object({ + key: Yup.string().optional(), + value: Yup.string().optional(), + }) + ), + auditEvents: Yup.array() + }), +}); + +const AuditSettingsForm: React.FC = (props) => { + const { orgPermissions, orgData, orgId, org, project } = props; + const classes = useStyles(); + const [data, setData] = useState(null); + + useEffect(() => { + const unsubscribe = Store.listen(function () { + const newStoreData = this.data; + + if (JSON.stringify(newStoreData) !== JSON.stringify(data)) { + const auth = newStoreData?.auth || null; + const orgProfile = newStoreData?.orgProfile || null; + const auditEvents = newStoreData?.auditEvents || null; + + if ( + auth?.token && + orgProfile && + orgProfile.orgId !== orgId && + !orgProfile.isProcessing + ) { + Actions.getOrgs(auth.token, orgId); + } + + if (auth?.token && auditEvents && !auditEvents.isProcessing) { + Actions.getAuditEvents(auth.token); + } + + setData(newStoreData); + } + }); + + Actions.refresh(); + + return () => { + unsubscribe(); + }; + }, [orgId, data, props.match.params.projectId]); + + const isAuditLogsSettingsAvailable = useMemo(() => { + const privileged_until = orgData?.priveleged_until; + return !!(orgData && privileged_until && new Date(privileged_until) > new Date() && orgData?.data?.plan === 'EE'); + + }, [orgData]) + + const formik = useFormik({ + enableReinitialize: true, + initialValues: { + siemSettings: { + enableSiemIntegration: Boolean(data?.orgProfile?.data?.siem_integration_enabled), + urlSchema: data?.orgProfile?.data?.siem_integration_url || '', + headers: data?.orgProfile?.data?.siem_integration_request_headers + ? Object.entries(data.orgProfile.data.siem_integration_request_headers).map(([key, value]) => ({ + key: key || '', + value: value || '', + })) as unknown as SiemSettings['headers'] + : [{ key: '', value: '' }], + auditEvents: data?.auditEvents?.data + ? data?.auditEvents?.data + ?.filter((event) => + data?.orgProfile?.data?.audit_events_to_log?.includes(event.event_name) + ) + ?.map((event) => ({ + id: event.id, + event_name: event.event_name, + label: event.label, + })) + : [], + }, + }, + validationSchema, + onSubmit: async (values, { setSubmitting }) => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + console.error('Validation errors:', errors); + setSubmitting(false); + return; // Stop submission if there are errors + } + + const currentOrgId = orgId || null; + const auth = data?.auth || null; + + if (auth) { + const params = formik.values.siemSettings; + try { + await Actions.updateAuditSettings(auth.token, currentOrgId, params); + } catch (error) { + const errorMessage = `Error updating audit settings: ${error}`; + Actions.showNotification(errorMessage, 'error'); + console.error('Error updating audit settings:', error); + } finally { + setSubmitting(false); + } + } + } + }); + + const isDisabled = useMemo(() => + !isAuditLogsSettingsAvailable || !formik.values.siemSettings.enableSiemIntegration, + [isAuditLogsSettingsAvailable, formik.values.siemSettings.enableSiemIntegration] + ); + + const testConnection = async () => { + try { + const auth = data?.auth || null; + + if (auth) { + const params = {...formik.values.siemSettings}; + if (formik.values.siemSettings.urlSchema) { + Actions.testSiemServiceConnection(auth.token, params); + } + } + } catch (error) { + console.error('Connection failed:', error); + } + }; + + const breadcrumbs = ( + + ); + + const pageTitle = ; + + if (orgPermissions && !orgPermissions.settingsOrganizationUpdate) { + return ( + <> + {breadcrumbs} + {pageTitle} + {messages.noPermissionPage} + + ); + } + + if (!data || (data && data.orgProfile && data.orgProfile.isProcessing) || (data && data.auditEvents && data.auditEvents.isProcessing)) { + return ( +
+ {breadcrumbs} + {pageTitle} + +
+ ); + } + + return ( + <> + {breadcrumbs} + {pageTitle} +
+ + + + {!isAuditLogsSettingsAvailable && + + Become an Enterprise customer + + +  to unlock audit settings + } + + + SIEM audit logs integration documentation + + + + + +

SIEM integration

+ + formik.setFieldValue( + 'siemSettings.enableSiemIntegration', + e.target.checked + ) + } + /> + } + label="Send audit events to SIEM system" + disabled={!isAuditLogsSettingsAvailable} + /> +

SIEM connection settings

+ +
+
+
+ + + + + + +

Select audit events to export

+ {data?.auditEvents?.data && + data?.auditEvents?.data?.map((event) => { + const isChecked = formik.values.siemSettings.auditEvents.some( + (e) => e.event_name === event.event_name + ); + + return ( +
+ { + const updatedAuditEvents = e.target.checked + ? [...formik.values.siemSettings.auditEvents, { ...event }] + : formik.values.siemSettings.auditEvents.filter( + (auditEvent) => auditEvent.event_name !== event.event_name + ); + + formik.setFieldValue('siemSettings.auditEvents', updatedAuditEvents); + }} + /> + } + label={event.label} + disabled={isDisabled} + /> +
+ ); + })} +
+
+
+ + + +
+
+
+ + ); +}; + +export default AuditSettingsForm diff --git a/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsFormWrapper.tsx b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsFormWrapper.tsx new file mode 100644 index 00000000..3ae26ec9 --- /dev/null +++ b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsFormWrapper.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import AuditSettingsForm from "./AuditSettingsForm"; + +export interface AuditSettingsFormProps { + mode?: string | undefined + project?: string | undefined + org?: string | number + orgId?: number + orgPermissions?: { + settingsOrganizationUpdate?: boolean + } + orgData?: { + priveleged_until: Date + chats_private_allowed: boolean + data?: { + plan?: string + } | null + } + match: { + params: { + project?: string + projectId?: string | number | undefined + org?: string + } + } +} + + + +export const AuditSettingsFormWrapper = (props: AuditSettingsFormProps) => { + return +} diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index c5a20e98..c1a2ad7f 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -87,6 +87,7 @@ import { PostgresClusterInstallWrapper } from 'components/PostgresClusterInstall import { PostgresClustersWrapper } from 'components/PostgresClusters/PostgresClustersWrapper' import cn from "classnames"; import { BotSettingsFormWrapper } from "../BotSettingsForm/BotSettingsFormWrapper"; +import { AuditSettingsFormWrapper } from "../AuditSettingsForm/AuditSettingsFormWrapper"; import { ExpandLess, ExpandMore } from "@material-ui/icons"; @@ -397,6 +398,7 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { + {orgPermissions && orgPermissions.auditLogView && ( + + + {icons.auditLogIcon} + + Audit + + )} {icons.settingsIcon} - Settings + Manage {activeMenuItems.has('settings') ? : } @@ -639,7 +665,7 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { activeClassName={parentProps.classes.menuItemActiveLink} to={'/' + org + '/settings'} > - General + General settings )} @@ -655,7 +681,7 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { activeClassName={parentProps.classes.menuItemActiveLink} to={'/' + org + '/assistant-settings'} > - AI Assistant + AI Assistant settings )} @@ -702,7 +728,7 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { )} - {orgPermissions && orgPermissions.auditLogView && ( + {orgData !== null && orgPermissions && Permissions.isAdmin(orgData) && orgPermissions.auditLogView && ( - Audit + Audit settings )} @@ -985,6 +1011,12 @@ function OrganizationWrapper(parentProps: OrganizationWrapperProps) { )} /> + ( + + )} + /> ( diff --git a/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx b/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx index 088cb735..47407451 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx @@ -206,7 +206,7 @@ export const IndexPageWrapper = (props: IndexPageProps) => { marginTop: '10px', }, menuSectionHeaderCollapsible: { - marginTop: 0 + //marginTop: 0 }, bottomFixedMenuItem: { fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', @@ -228,7 +228,12 @@ export const IndexPageWrapper = (props: IndexPageProps) => { color: '#000000', display: 'inline-flex', alignItems: 'center', - whiteSpace: 'nowrap' + whiteSpace: 'nowrap', + margin: 0 + }, + menuSectionHeaderLinkCollapsible: { + paddingTop: '10px!important', + paddingBottom: '10px!important', }, menuSectionHeaderActiveLink: { textDecoration: 'none', diff --git a/ui/packages/platform/src/components/SIEMIntegrationForm/SIEMIntegrationForm.tsx b/ui/packages/platform/src/components/SIEMIntegrationForm/SIEMIntegrationForm.tsx new file mode 100644 index 00000000..8f27fdc3 --- /dev/null +++ b/ui/packages/platform/src/components/SIEMIntegrationForm/SIEMIntegrationForm.tsx @@ -0,0 +1,181 @@ +import React, { useMemo, useState } from 'react'; +import { TextField, Grid, IconButton } from '@mui/material'; +import { Button, makeStyles } from "@material-ui/core"; +import { styles } from "@postgres.ai/shared/styles/styles"; +import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline'; +import { FormikErrors, useFormik } from "formik"; +import { FormValues } from "../AuditSettingsForm/AuditSettingsForm"; + +const useStyles = makeStyles({ + textField: { + ...styles.inputField, + maxWidth: 450, + }, + requestHeadersContainer: { + paddingTop: '8px!important' + }, + label: { + color: '#000!important', + margin: 0 + }, + requestHeadersTextFieldContainer: { + flexBasis: 'calc(100% / 2 - 20px)!important', + width: 'calc(100% / 2 - 20px)!important', + }, + requestHeadersIconButtonContainer: { + width: '32px!important', + height: '32px!important', + padding: '0!important', + marginLeft: 'auto!important', + marginTop: '12px!important', + '& button': { + width: 'inherit', + height: 'inherit' + } + } +}) + +interface SIEMIntegrationFormProps { + formik: ReturnType>; + disabled: boolean +} + +export const SIEMIntegrationForm: React.FC = ({ formik, disabled }) => { + const classes = useStyles(); + const [isFocused, setIsFocused] = useState(false); + const [focusedHeaderIndex, setFocusedHeaderIndex] = useState(null); + + const getTruncatedUrl = (url: string) => { + const parts = url.split('/'); + return parts.length > 3 ? parts.slice(0, 3).join('/') + '/*****/' : url; + }; + + const handleHeaderValueDisplay = (index: number, value: string) => { + if (focusedHeaderIndex === index) { + return value; + } + if (value.length) { + return "*****"; + } else { + return '' + } + }; + + const handleFocusHeaderValue = (index: number) => setFocusedHeaderIndex(index); + const handleBlurHeaderValue = () => setFocusedHeaderIndex(null); + + const handleFocus = () => setIsFocused(true); + const handleBlur = () => setIsFocused(false); + + const handleHeaderChange = (index: number, field: 'key' | 'value', value: string) => { + const headers = formik.values.siemSettings.headers || []; + const updatedHeaders = [...headers]; + updatedHeaders[index] = { + ...updatedHeaders[index], + [field]: value, + }; + formik.setFieldValue('siemSettings.headers', updatedHeaders); + }; + + const addHeader = () => { + const headers = formik.values.siemSettings.headers || []; + const updatedHeaders = [...headers, { key: '', value: '' }]; + formik.setFieldValue('siemSettings.headers', updatedHeaders); + }; + + const removeHeader = (index: number) => { + const updatedHeaders = formik.values.siemSettings?.headers?.filter((_, i) => i !== index); + formik.setFieldValue('siemSettings.headers', updatedHeaders); + }; + + return ( + + + formik.setFieldValue('siemSettings.urlSchema', e.target.value)} + onFocus={handleFocus} + onBlur={(e) => { + formik.handleBlur(e); + handleBlur(); + }} + margin="normal" + fullWidth + placeholder="https://{siem-host}/{path}" + inputProps={{ + name: 'siemSettings.urlSchema', + id: 'urlSchemaTextField', + shrink: 'true', + }} + InputLabelProps={{ + shrink: true, + }} + disabled={disabled} + error={formik.touched.siemSettings?.urlSchema && !!formik.errors.siemSettings?.urlSchema} + helperText={formik.touched.siemSettings?.urlSchema && formik.errors.siemSettings?.urlSchema} + /> + + +

Request headers

+ {formik.values.siemSettings.headers.map((header, index) => ( + + + handleHeaderChange(index, 'key', e.target.value)} + placeholder="Authorization" + inputProps={{ + name: `siemSettings.headers[${index}].key`, + id: `requestHeaderKeyField${index}`, + shrink: 'true', + }} + InputLabelProps={{ + shrink: true, + }} + margin="normal" + disabled={disabled} + /> + + + handleHeaderChange(index, 'value', e.target.value)} + onFocus={() => handleFocusHeaderValue(index)} + onBlur={handleBlurHeaderValue} + placeholder="token" + inputProps={{ + name: `siemSettings.headers[${index}].value`, + id: `requestHeaderValueField${index}`, + shrink: 'true', + }} + InputLabelProps={{ + shrink: true, + }} + margin="normal" + disabled={disabled} + /> + + + removeHeader(index)} disabled={disabled}> + + + + + ))} + +
+
+ ); +}; \ No newline at end of file diff --git a/ui/packages/platform/src/stores/store.js b/ui/packages/platform/src/stores/store.js index 7ff47b6b..222f2d22 100644 --- a/ui/packages/platform/src/stores/store.js +++ b/ui/packages/platform/src/stores/store.js @@ -236,7 +236,8 @@ const initialState = { isLogDownloading: false, logs: {} }, - auditLog: storeItem + auditLog: storeItem, + auditEvents: {...storeItem} }; const Store = Reflux.createStore({ @@ -594,6 +595,58 @@ const Store = Reflux.createStore({ }, + onUpdateAuditSettingsFailed: function (error) { + this.data.orgProfile.isUpdating = false; + this.data.orgProfile.updateError = true; + this.data.orgProfile.updateErrorMessage = error.message; + this.trigger(this.data); + }, + + onUpdateAuditSettingsProgressed: function (data) { + this.data.orgProfile.updateErrorFields = null; + this.data.orgProfile.isUpdating = true; + + this.trigger(this.data); + }, + + onUpdateAuditSettingsCompleted: function (data) { + this.data.orgProfile.isUpdating = false; + this.data.orgProfile.updateErrorMessage = this.getError(data); + this.data.orgProfile.updateError = !!this.data.orgProfile.updateErrorMessage; + + if (!this.data.orgProfile.updateError && data.length > 0) { + this.data.orgProfile.updateErrorFields = null; + this.data.orgProfile.data = data[0]; + Actions.getUserProfile(this.data.auth.token); + Actions.getOrgs(this.data.auth.token, this.data.orgProfile.orgId); + Actions.showNotification('Audit settings successfully saved.', 'success'); + } + + this.trigger(this.data); + }, + + onTestSiemServiceConnectionFailed: function (error) { + this.data.orgProfile.isUpdating = false; + this.trigger(this.data); + }, + + onTestSiemServiceConnectionProgressed: function (data) { + this.data.orgProfile.isUpdating = true; + this.trigger(this.data); + }, + + onTestSiemServiceConnectionCompleted: function (data) { + this.data.orgProfile.isUpdating = false; + if (data && data.test_siem_connection && data.test_siem_connection.status && data.test_siem_connection.status < 300) { + Actions.showNotification('Connection successful', 'success'); + } else { + Actions.showNotification('Connection error', 'error'); + } + + this.trigger(this.data); + }, + + onCreateOrgFailed: function (error) { this.data.orgProfile.isUpdating = false; this.data.orgProfile.updateError = true; @@ -2961,7 +3014,40 @@ const Store = Reflux.createStore({ } this.trigger(this.data); - } + }, + + onGetAuditEventsFailed: function (error) { + this.data.auditEvents.isProcessing = false; + this.data.auditEvents.error = true; + this.data.auditEvents.errorMessage = error.message; + this.trigger(this.data); + }, + + onGetAuditEventsProgressed: function (data) { + this.data.auditEvents.isProcessing = true; + + this.trigger(this.data); + }, + + onGetAuditEventsCompleted: function (data) { + this.data.auditEvents.isProcessing = false; + this.data.auditEvents.errorMessage = this.getError(data.data); + this.data.auditEvents.error = this.data.orgProfile.errorMessage; + + if (!this.data.auditEvents.error) { + if (data.data.length > 0) { + this.data.auditEvents.isProcessed = true; + this.data.auditEvents = {...data}; + } else { + this.data.auditEvents.error = true; + this.data.auditEvents.errorMessage = + 'You do not have permission to view this page.'; + this.data.auditEvents.errorCode = 403; + } + } + + this.trigger(this.data); + }, }); diff --git a/ui/packages/shared/styles/icons.tsx b/ui/packages/shared/styles/icons.tsx index fd8da26c..d1d52fc5 100644 --- a/ui/packages/shared/styles/icons.tsx +++ b/ui/packages/shared/styles/icons.tsx @@ -1899,5 +1899,13 @@ export const icons = { + ), + auditLogIcon: ( + + + ) } From b8f572d423680ddd9d5c55f26c0f193b994dc729 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 26 Dec 2024 21:26:00 +0000 Subject: [PATCH 095/111] fix (ui): Audit logs page update --- .../platform/src/components/Audit/Audit.tsx | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/ui/packages/platform/src/components/Audit/Audit.tsx b/ui/packages/platform/src/components/Audit/Audit.tsx index 7e6b337d..692c09ff 100644 --- a/ui/packages/platform/src/components/Audit/Audit.tsx +++ b/ui/packages/platform/src/components/Audit/Audit.tsx @@ -45,12 +45,12 @@ interface AuditWithStylesProps extends AuditProps { export interface AuditLogData { id: number - data_before: string - data_after: string action: string actor: string action_data: { processed_row_count: number + data_before: Record[] + data_after: Record[] } created_at: string table_name: string @@ -193,9 +193,9 @@ class Audit extends Component { } getChangesTitle = (r: AuditLogData) => { - const displayedCount = r.data_before - ? r.data_before?.length - : r.data_after?.length + const displayedCount = r.action_data && r.action_data.data_before + ? r.action_data.data_before?.length + : r.action_data?.data_after?.length const objCount = r.action_data && r.action_data.processed_row_count ? r.action_data.processed_row_count @@ -243,15 +243,6 @@ class Audit extends Component { const pageTitle = ( 0 - ? { - filterValue: this.state.filterValue, - filterHandler: this.filterInputHandler, - placeholder: 'Search audit log', - } - : null - } /> ) @@ -310,7 +301,7 @@ class Audit extends Component { {this.formatAction(r)} - {(r.data_before || r.data_after) && ( + {((r.action_data && r.action_data.data_before) || (r.action_data && r.action_data.data_after)) && (
{ - {r.data_before && ( + {r.action_data && r.action_data.data_before && (
{this.getDataSectionTitle(r, true)} { multiline fullWidth value={JSON.stringify( - r.data_before, + r.action_data.data_before, null, 4, )} @@ -347,7 +338,7 @@ class Audit extends Component { />
)} - {r.data_after && ( + {r.action_data && r.action_data.data_after && (
{this.getDataSectionTitle(r, false)} { multiline fullWidth value={JSON.stringify( - r.data_after, + r.action_data.data_after, null, 4, )} From 83c7602788753810140bdbdebf3758013192092c Mon Sep 17 00:00:00 2001 From: Nikolay Samokhvalov Date: Thu, 26 Dec 2024 22:16:06 +0000 Subject: [PATCH 096/111] (Platform) left menu: SQL Optimization -> Joe bot --- ui/packages/platform/src/components/IndexPage/IndexPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index c1a2ad7f..6d848ffe 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -466,7 +466,7 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { {icons.sqlOptimizationIcon} - SQL Optimization + Joe bot {activeMenuItems.has('sqlOptimization') ? : } From de1aea66c5538079e738fa896edbf169b2af4d01 Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Thu, 2 Jan 2025 17:40:14 +0500 Subject: [PATCH 097/111] Update simple-install service address --- ui/packages/platform/src/helpers/simpleInstallRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/packages/platform/src/helpers/simpleInstallRequest.ts b/ui/packages/platform/src/helpers/simpleInstallRequest.ts index ba9be5c3..5b4a7e90 100644 --- a/ui/packages/platform/src/helpers/simpleInstallRequest.ts +++ b/ui/packages/platform/src/helpers/simpleInstallRequest.ts @@ -5,7 +5,7 @@ import { const sign = require('jwt-encode') -export const SI_API_SERVER = 'https://si.aws.postgres.ai' +export const SI_API_SERVER = 'https://si.dblab.dev' export const JWT_SECRET = 'some-jwt-secret' export const JWT_PAYLOAD = (userID?: number) => ({ From a6e1dae7462f194676855534462f17dd2679cf4f Mon Sep 17 00:00:00 2001 From: Vitaliy Kukharik Date: Tue, 14 Jan 2025 14:02:51 +0000 Subject: [PATCH 098/111] Platform: Update dle-se-ansible image tag to v1.4; DBLab v4.0.0-beta.0 --- .../platform/src/components/DbLabInstanceForm/utils/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx index 18e41213..3fc3d48c 100644 --- a/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx +++ b/ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx @@ -8,9 +8,9 @@ import { useCloudProviderProps } from 'hooks/useCloudProvider' const API_SERVER = process.env.REACT_APP_API_SERVER export const DEBUG_API_SERVER = 'https://v2.postgres.ai/api/general' -export const availableTags = ['3.5.0', '3.4.0', '4.0.0-alpha.6'] +export const availableTags = ['3.5.0', '3.4.0', '4.0.0-beta.0'] -export const sePackageTag = 'v1.3' +export const sePackageTag = 'v1.4' export const dockerRunCommand = (provider: string) => { switch (provider) { From 5e1e3bf430f99cd9cecb44857b6f8387fad46bef Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Wed, 15 Jan 2025 17:16:00 +0000 Subject: [PATCH 099/111] feat (ui): Display user ID in org members page --- ui/packages/platform/src/components/OrgMembers/OrgMembers.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/packages/platform/src/components/OrgMembers/OrgMembers.tsx b/ui/packages/platform/src/components/OrgMembers/OrgMembers.tsx index a86bb6f1..70313921 100644 --- a/ui/packages/platform/src/components/OrgMembers/OrgMembers.tsx +++ b/ui/packages/platform/src/components/OrgMembers/OrgMembers.tsx @@ -361,6 +361,7 @@ class OrgSettings extends Component< + ID Email Role First name @@ -372,6 +373,7 @@ class OrgSettings extends Component< {filteredUsers.map((u: UsersType) => { return ( + {u.id} {u.email} {this.roleSelector(u)} From f2cddf67bf59293b9a227c0d03e3c4881a9eee92 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 17 Jan 2025 17:58:33 +0000 Subject: [PATCH 100/111] Send email notifications from AI --- ui/packages/platform/src/actions/actions.js | 37 +++++ ui/packages/platform/src/api/api.js | 25 +++ .../BotSettingsForm/BotSettingsForm.tsx | 2 +- .../platform/src/pages/Bot/BotWrapper.tsx | 4 +- .../pages/Bot/Messages/Message/Message.tsx | 55 +++++-- .../src/pages/Bot/Messages/Messages.tsx | 7 +- ui/packages/platform/src/pages/Bot/hooks.tsx | 30 +++- ui/packages/platform/src/pages/Bot/index.tsx | 2 +- .../src/pages/Profile/ProfileWrapper.tsx | 22 +++ .../platform/src/pages/Profile/index.jsx | 150 ++++++++++++++---- ui/packages/platform/src/stores/store.js | 26 +++ .../platform/src/types/api/entities/bot.ts | 7 +- .../shared/components/TextField/index.tsx | 14 +- 13 files changed, 327 insertions(+), 54 deletions(-) diff --git a/ui/packages/platform/src/actions/actions.js b/ui/packages/platform/src/actions/actions.js index f16a8242..a5d7b018 100644 --- a/ui/packages/platform/src/actions/actions.js +++ b/ui/packages/platform/src/actions/actions.js @@ -38,6 +38,7 @@ const Actions = Reflux.createActions([{ ASYNC_ACTION: ASYNC_ACTION, doAuth: ASYNC_ACTION, getUserProfile: ASYNC_ACTION, + updateUserProfile: ASYNC_ACTION, getAccessTokens: ASYNC_ACTION, getAccessToken: ASYNC_ACTION, hideGeneratedAccessToken: {}, @@ -267,6 +268,42 @@ Actions.getUserProfile.listen(function (token) { ); }); +Actions.updateUserProfile.listen(function (token, data) { + let action = this; + + if (!api) { + settings.init(function () { + api = new Api(settings); + }); + } + + this.progressed(); + + timeoutPromise(REQUEST_TIMEOUT, api.updateUserProfile(token, data)) + .then(result => { + result.json() + .then(json => { + if (json) { + action.completed({ data: json?.result }); + } else { + action.failed(new Error('wrong_reply')); + } + }) + .catch(err => { + console.error(err); + action.failed(new Error('wrong_reply')); + }); + }) + .catch(err => { + console.error(err); + if (err && err.message && err.message === 'timeout') { + action.failed(new Error('failed_fetch')); + } else { + action.failed(new Error('wrong_reply')); + } + }); +}); + Actions.getAccessTokens.listen(function (token, orgId) { let action = this; diff --git a/ui/packages/platform/src/api/api.js b/ui/packages/platform/src/api/api.js index 14d2d033..36dbdf30 100644 --- a/ui/packages/platform/src/api/api.js +++ b/ui/packages/platform/src/api/api.js @@ -112,6 +112,31 @@ class Api { }); } + updateUserProfile(token, data) { + let headers = { + Authorization: 'Bearer ' + token, + Accept: 'application/vnd.pgrst.object+json' + }; + + let body = {}; + + if (data.is_chats_email_notifications_enabled !== 'undefined') { + body.chats_email_notifications_enabled = data.is_chats_email_notifications_enabled; + } + + if (data.first_name !== 'undefined') { + body.first_name = data.first_name; + } + + if (data.last_name !== 'undefined') { + body.last_name = data.last_name; + } + + return this.post(`${this.apiServer}/rpc/update_user_profile`, body, { + headers: headers + }); + } + getAccessTokens(token, orgId) { let params = {}; let headers = { diff --git a/ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx b/ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx index f4da9c5e..ddab1e03 100644 --- a/ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx +++ b/ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx @@ -161,7 +161,7 @@ const BotSettingsForm: React.FC = (props) => { enableReinitialize: true, initialValues: { threadVisibility: - data?.orgProfile?.data?.is_chat_public_by_default ? 'public' : 'private', + data?.orgProfile?.data?.is_chat_public_by_default ? 'public' : 'private' }, onSubmit: () => { const currentOrgId = orgId || null; diff --git a/ui/packages/platform/src/pages/Bot/BotWrapper.tsx b/ui/packages/platform/src/pages/Bot/BotWrapper.tsx index fd8e7837..20d088d9 100644 --- a/ui/packages/platform/src/pages/Bot/BotWrapper.tsx +++ b/ui/packages/platform/src/pages/Bot/BotWrapper.tsx @@ -7,6 +7,7 @@ export interface BotWrapperProps { orgId?: number; envData: { info?: { + id?: number | null user_name?: string } }; @@ -38,7 +39,8 @@ export const BotWrapper = (props: BotWrapperProps) => { args={{ threadId: props.match.params.threadId, orgId: props.orgData.id, - isPublicByDefault: props.orgData.is_chat_public_by_default + isPublicByDefault: props.orgData.is_chat_public_by_default, + userId: props.envData.info?.id, }}> diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx index 5aa894c4..7a7c6a2d 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react' +import React, { useEffect, useMemo, useRef, useState } from 'react' import cn from "classnames"; import ReactMarkdown, { Components } from "react-markdown"; import rehypeRaw from "rehype-raw"; @@ -9,8 +9,9 @@ import { icons } from "@postgres.ai/shared/styles/icons"; import { DebugDialog } from "../../DebugDialog/DebugDialog"; import { CodeBlock } from "./CodeBlock"; import { disallowedHtmlTagsForMarkdown, permalinkLinkBuilder } from "../../utils"; -import { StateMessage } from "../../../../types/api/entities/bot"; +import { MessageStatus, StateMessage } from "../../../../types/api/entities/bot"; import { MermaidDiagram } from "./MermaidDiagram"; +import { useAiBot } from "../../hooks"; type BaseMessageProps = { @@ -20,17 +21,19 @@ type BaseMessageProps = { name?: string; isLoading?: boolean; formattedTime?: string; - aiModel?: string - stateMessage?: StateMessage | null - isCurrentStreamMessage?: boolean + aiModel?: string; + stateMessage?: StateMessage | null; + isCurrentStreamMessage?: boolean; isPublic?: boolean; + threadId?: string; + status?: MessageStatus } type AiMessageProps = BaseMessageProps & { isAi: true; content: string; - aiModel: string - isCurrentStreamMessage?: boolean + aiModel: string; + isCurrentStreamMessage?: boolean; } type HumanMessageProps = BaseMessageProps & { @@ -42,8 +45,8 @@ type HumanMessageProps = BaseMessageProps & { type LoadingMessageProps = BaseMessageProps & { isLoading: true; isAi: true; - content?: undefined - stateMessage: StateMessage | null + content?: undefined; + stateMessage: StateMessage | null; } type MessageProps = AiMessageProps | HumanMessageProps | LoadingMessageProps; @@ -261,14 +264,44 @@ export const Message = React.memo((props: MessageProps) => { aiModel, stateMessage, isCurrentStreamMessage, - isPublic + isPublic, + threadId, + status } = props; + const { updateMessageStatus } = useAiBot() + + const elementRef = useRef(null); + + const [isDebugVisible, setDebugVisible] = useState(false); const classes = useStyles(); + useEffect(() => { + if (!isAi || isCurrentStreamMessage || status === 'read') return; + + const observer = new IntersectionObserver( + (entries) => { + const entry = entries[0]; + if (entry.isIntersecting && threadId && id) { + updateMessageStatus(threadId, id, 'read'); + observer.disconnect(); + } + }, + { threshold: 0.1 } + ); + + if (elementRef.current) { + observer.observe(elementRef.current); + } + + return () => { + observer.disconnect(); + }; + }, [id, updateMessageStatus, isCurrentStreamMessage, isAi, threadId, status]); + const contentToRender: string = content?.replace(/\n/g, ' \n') || '' const toggleDebugDialog = () => { @@ -301,7 +334,7 @@ export const Message = React.memo((props: MessageProps) => { onClose={toggleDebugDialog} messageId={id} />} -
+
{isAi ? { +export const Messages = React.memo(({orgId, threadId}: {orgId: number, threadId?: string}) => { const { messages, loading: isLoading, @@ -254,7 +254,8 @@ export const Messages = React.memo(({orgId}: {orgId: number}) => { created_at, content, ai_model, - is_public + is_public, + status } = message; let name = 'You'; @@ -283,6 +284,8 @@ export const Messages = React.memo(({orgId}: {orgId: number}) => { formattedTime={formattedTime} aiModel={ai_model} isPublic={is_public} + threadId={threadId} + status={status} /> ) })} diff --git a/ui/packages/platform/src/pages/Bot/hooks.tsx b/ui/packages/platform/src/pages/Bot/hooks.tsx index 840b3785..fc08ecb9 100644 --- a/ui/packages/platform/src/pages/Bot/hooks.tsx +++ b/ui/packages/platform/src/pages/Bot/hooks.tsx @@ -14,7 +14,7 @@ import { AiModel, StateMessage, StreamMessage, - ErrorMessage + ErrorMessage, MessageStatus } from "../../types/api/entities/bot"; import {getChatsWithWholeThreads} from "../../api/bot/getChatsWithWholeThreads"; import {getChats} from "api/bot/getChats"; @@ -73,16 +73,18 @@ type UseAiBotReturnType = { isStreamingInProcess: boolean; currentStreamMessage: StreamMessage | null; errorMessage: ErrorMessage | null; + updateMessageStatus: (threadId: string, messageId: string, status: MessageStatus) => void } type UseAiBotArgs = { threadId?: string; orgId?: number isPublicByDefault?: boolean + userId?: number | null } export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => { - const { threadId, orgId, isPublicByDefault } = args; + const { threadId, orgId, isPublicByDefault, userId } = args; const { showMessage, closeSnackbar } = useAlertSnackbar(); const { aiModels, @@ -413,6 +415,27 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => })) } + const updateMessageStatus = (threadId: string, messageId: string, status: MessageStatus) => { + wsSendMessage(JSON.stringify({ + action: 'message_status_update', + payload: { + thread_id: threadId, + message_id: messageId, + read_by: userId, + status + } + })) + if (messages && messages.length > 0) { + const updatedMessages = messages.map((item) => { + if (item.id === messageId) { + item["status"] = status + } + return item + }); + setMessages(updatedMessages) + } + } + const getDebugMessagesForWholeThread = async () => { setDebugMessagesLoading(true) if (threadId) { @@ -478,7 +501,8 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => stateMessage, isStreamingInProcess, currentStreamMessage, - errorMessage + errorMessage, + updateMessageStatus } } diff --git a/ui/packages/platform/src/pages/Bot/index.tsx b/ui/packages/platform/src/pages/Bot/index.tsx index db6a6269..9bf5c955 100644 --- a/ui/packages/platform/src/pages/Bot/index.tsx +++ b/ui/packages/platform/src/pages/Bot/index.tsx @@ -268,7 +268,7 @@ export const BotPage = (props: BotPageProps) => { - + { marginLeft: theme.spacing(1), marginRight: theme.spacing(1), }, + formControlLabel: { + marginLeft: theme.spacing(0), + marginRight: theme.spacing(1), + }, + formControlLabelCheckbox: { + '& svg': { + fontSize: 18 + } + }, + updateButtonContainer: { + marginTop: theme.spacing(3), + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + }, + label: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(1), + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + color: '#000!important', + fontWeight: 'bold', + }, dense: { marginTop: 16, }, diff --git a/ui/packages/platform/src/pages/Profile/index.jsx b/ui/packages/platform/src/pages/Profile/index.jsx index 045996bf..fe07f419 100644 --- a/ui/packages/platform/src/pages/Profile/index.jsx +++ b/ui/packages/platform/src/pages/Profile/index.jsx @@ -8,7 +8,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Grid from '@material-ui/core/Grid'; - +import * as Yup from 'yup'; import { TextField } from '@postgres.ai/shared/components/TextField'; import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner'; @@ -17,9 +17,18 @@ import Actions from 'actions/actions'; import { ErrorWrapper } from 'components/Error/ErrorWrapper'; import ConsolePageTitle from 'components/ConsolePageTitle'; import { Head, createTitle } from 'components/Head'; +import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; +import CheckBoxIcon from '@material-ui/icons/CheckBox'; +import {Button, Checkbox, FormControlLabel, InputLabel} from "@material-ui/core"; +import {Form, Formik} from "formik"; const PAGE_NAME = 'Profile'; +const validationSchema = Yup.object({ + first_name: Yup.string().required('First name is required'), + last_name: Yup.string().required('Last name is required'), +}); + class Profile extends Component { componentDidMount() { const that = this; @@ -31,6 +40,14 @@ class Profile extends Component { that.setState({ data: this.data }); + if (userProfile && !userProfile.isProcessing && userProfile.data.info) { + that.setState({ + is_chats_email_notifications_enabled: userProfile.data.info.chats_email_notifications_enabled, + first_name: userProfile.data.info.first_name, + last_name: userProfile.data.info.last_name + }); + } + if (auth && auth.token && !userProfile.isProcessed && !userProfile.isProcessing && !userProfile.error) { Actions.getUserProfile(auth.token); @@ -44,10 +61,28 @@ class Profile extends Component { this.unsubscribe(); } + handleSaveSettings = (values) => { + const auth = this.state.data?.auth; + if (auth) { + Actions.updateUserProfile(auth.token, { + is_chats_email_notifications_enabled: values.is_chats_email_notifications_enabled, + first_name: values.first_name, + last_name: values.last_name, + }); + } + }; + render() { const { classes } = this.props; const data = this.state && this.state.data ? this.state.data.userProfile : null; + const initialValues = { + first_name: data?.data?.info?.first_name || '', + last_name: data?.data?.info?.last_name || '', + is_chats_email_notifications_enabled: data?.data?.info?.chats_email_notifications_enabled || false, + }; + + const headRendered = ( ); @@ -85,38 +120,89 @@ class Profile extends Component { { headRendered } {pageTitle} - - - - - - + {({ values, handleChange, setFieldValue, errors, touched }) => ( +
+ + + + + + Notifications settings + + } + checkedIcon={} + name="is_chats_email_notifications_enabled" + className={classes.formControlLabelCheckbox} + checked={values.is_chats_email_notifications_enabled} + onChange={(event) => + setFieldValue('is_chats_email_notifications_enabled', event.target.checked) + } + /> + } + label="Notify about new messages in the AI Assistant" + /> + + + + + + )} +
); } diff --git a/ui/packages/platform/src/stores/store.js b/ui/packages/platform/src/stores/store.js index 222f2d22..7cd9dfce 100644 --- a/ui/packages/platform/src/stores/store.js +++ b/ui/packages/platform/src/stores/store.js @@ -386,6 +386,32 @@ const Store = Reflux.createStore({ this.trigger(this.data); }, + onUpdateUserProfileFailed: function (error) { + this.data.userProfile.isProcessing = false; + this.data.userProfile.error = true; + this.data.userProfile.errorMessage = error.message; + Actions.showNotification(error.message, 'error'); + this.trigger(this.data); + }, + + onUpdateUserProfileProgressed: function () { + this.data.userProfile.isProcessing = true; + this.trigger(this.data); + }, + + onUpdateUserProfileCompleted: function (data) { + this.data.userProfile.isProcessing = false; + this.data.userProfile.errorMessage = this.getError(data); + this.data.userProfile.error = !!this.data.userProfile.errorMessage; + + if (!this.data.userProfile.error && data?.data?.length > 0) { + this.data.userProfile.data = data?.data?.[0]; + this.data.userProfile.isProcessed = true; + Actions.showNotification('Profile settings successfully saved.', 'success'); + } + + this.trigger(this.data); + }, onGetOrgsFailed: function (error) { this.data.orgProfile.isProcessing = false; diff --git a/ui/packages/platform/src/types/api/entities/bot.ts b/ui/packages/platform/src/types/api/entities/bot.ts index 773859a6..c30dd25d 100644 --- a/ui/packages/platform/src/types/api/entities/bot.ts +++ b/ui/packages/platform/src/types/api/entities/bot.ts @@ -18,11 +18,12 @@ export type BotMessage = { last_name: string | null display_name: string | null slack_profile: string | null - user_id: string + user_id: number org_id: string thread_id: string type: 'message' | undefined ai_model: string + status?: MessageStatus } export type BotMessageWithDebugInfo = BotMessage & { @@ -54,4 +55,6 @@ export type ErrorMessage = { type: 'error' message: string thread_id: string -} \ No newline at end of file +} + +export type MessageStatus = 'read' | 'new' | null \ No newline at end of file diff --git a/ui/packages/shared/components/TextField/index.tsx b/ui/packages/shared/components/TextField/index.tsx index 9500eb3f..ab9af54e 100644 --- a/ui/packages/shared/components/TextField/index.tsx +++ b/ui/packages/shared/components/TextField/index.tsx @@ -35,6 +35,8 @@ export type TextFieldProps = { placeholder?: string onBlur?: TextFieldPropsBase['onBlur'] onFocus?: TextFieldPropsBase['onFocus'] + name?: TextFieldPropsBase['name'] + helperText?: TextFieldPropsBase['helperText'] } const useStyles = makeStyles( @@ -51,6 +53,9 @@ const useStyles = makeStyles( input: { padding: '8px', }, + helperText: { + fontSize: 12 + } }, { index: 1 }, ) @@ -72,7 +77,7 @@ export const TextField = (props: TextFieldProps) => { value={props.value} margin="normal" fullWidth={props.fullWidth} - classes={{}} + classes={{ }} InputProps={{ ...props.InputProps, @@ -92,6 +97,11 @@ export const TextField = (props: TextFieldProps) => { ...props.InputLabelProps, }} + FormHelperTextProps={{ + classes: { + root: classes.helperText + } + }} onChange={props.onChange} children={props.children} select={props.select} @@ -100,6 +110,8 @@ export const TextField = (props: TextFieldProps) => { placeholder={props.placeholder} onBlur={props.onBlur} onFocus={props.onFocus} + name={props.name} + helperText={props.helperText} /> ) } From f17ef0f9bcae451503abbbf74bf49d61a9dc0bd6 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Fri, 17 Jan 2025 20:00:13 +0000 Subject: [PATCH 101/111] hotfix: fixed data parsing received from the API in response to a profile update --- ui/packages/platform/src/actions/actions.js | 2 +- ui/packages/platform/src/pages/Profile/index.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/packages/platform/src/actions/actions.js b/ui/packages/platform/src/actions/actions.js index a5d7b018..a6c6ae4e 100644 --- a/ui/packages/platform/src/actions/actions.js +++ b/ui/packages/platform/src/actions/actions.js @@ -284,7 +284,7 @@ Actions.updateUserProfile.listen(function (token, data) { result.json() .then(json => { if (json) { - action.completed({ data: json?.result }); + action.completed({ data: json }); } else { action.failed(new Error('wrong_reply')); } diff --git a/ui/packages/platform/src/pages/Profile/index.jsx b/ui/packages/platform/src/pages/Profile/index.jsx index fe07f419..1a95715c 100644 --- a/ui/packages/platform/src/pages/Profile/index.jsx +++ b/ui/packages/platform/src/pages/Profile/index.jsx @@ -180,7 +180,7 @@ class Profile extends Component { } /> } - label="Notify about new messages in the AI Assistant" + label="Send an email notification if a new message from AI Assistant remains unread for more than one minute" /> Date: Thu, 30 Jan 2025 22:12:50 +0000 Subject: [PATCH 102/111] Consulting section in Console --- ui/packages/platform/package.json | 1 + .../src/components/IndexPage/IndexPage.tsx | 25 ++ .../platform/src/components/types/index.ts | 1 + .../pages/Consulting/ConsultingWrapper.tsx | 25 ++ .../platform/src/pages/Consulting/index.tsx | 223 ++++++++++++++++++ .../platform/src/pages/Consulting/utils.ts | 30 +++ ui/packages/platform/src/stores/consulting.ts | 101 ++++++++ ui/packages/shared/styles/icons.tsx | 6 + ui/pnpm-lock.yaml | 8 + 9 files changed, 420 insertions(+) create mode 100644 ui/packages/platform/src/pages/Consulting/ConsultingWrapper.tsx create mode 100644 ui/packages/platform/src/pages/Consulting/index.tsx create mode 100644 ui/packages/platform/src/pages/Consulting/utils.ts create mode 100644 ui/packages/platform/src/stores/consulting.ts diff --git a/ui/packages/platform/package.json b/ui/packages/platform/package.json index fcdb53d2..71ef15df 100644 --- a/ui/packages/platform/package.json +++ b/ui/packages/platform/package.json @@ -55,6 +55,7 @@ "mobx": "^6.3.2", "mobx-react-lite": "^3.2.0", "moment": "^2.24.0", + "postgres-interval": "^4.0.2", "prop-types": "^15.7.2", "qs": "^6.11.0", "react": "^17.0.2", diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index 6d848ffe..8b22804f 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -73,6 +73,7 @@ import { NotificationWrapper } from 'components/Notification/NotificationWrapper import { SharedUrlWrapper } from 'components/SharedUrl/SharedUrlWrapper' import { ShareUrlDialogWrapper } from 'components/ShareUrlDialog/ShareUrlDialogWrapper' import { BotWrapper } from "pages/Bot/BotWrapper"; +import { ConsultingWrapper } from "pages/Consulting/ConsultingWrapper"; import Actions from '../../actions/actions' import JoeConfig from '../JoeConfig' @@ -623,6 +624,23 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { Audit )} + + + + {icons.consultingIcon} + + Consulting + + ; }} /> + ( + + )} + /> ( diff --git a/ui/packages/platform/src/components/types/index.ts b/ui/packages/platform/src/components/types/index.ts index 5e410daa..cb434205 100644 --- a/ui/packages/platform/src/components/types/index.ts +++ b/ui/packages/platform/src/components/types/index.ts @@ -40,6 +40,7 @@ export interface Orgs { owner_user_id: number is_chat_public_by_default: boolean chats_private_allowed: boolean + consulting_type: string | null data: { plan: string } | null diff --git a/ui/packages/platform/src/pages/Consulting/ConsultingWrapper.tsx b/ui/packages/platform/src/pages/Consulting/ConsultingWrapper.tsx new file mode 100644 index 00000000..bcf7e7c1 --- /dev/null +++ b/ui/packages/platform/src/pages/Consulting/ConsultingWrapper.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { Consulting } from "./index"; +import { RouteComponentProps } from "react-router"; + +export interface ConsultingWrapperProps { + orgId?: number; + history: RouteComponentProps['history'] + project?: string + match: { + params: { + org?: string + } + } + orgData: { + consulting_type: string | null + alias: string + role: { + id: number + } + } +} + +export const ConsultingWrapper = (props: ConsultingWrapperProps) => { + return ; +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Consulting/index.tsx b/ui/packages/platform/src/pages/Consulting/index.tsx new file mode 100644 index 00000000..fc5bb9b7 --- /dev/null +++ b/ui/packages/platform/src/pages/Consulting/index.tsx @@ -0,0 +1,223 @@ +import React, { useEffect } from "react"; +import ConsolePageTitle from "../../components/ConsolePageTitle"; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import { Grid, Paper, Typography } from "@mui/material"; +import Button from "@mui/material/Button"; +import Box from "@mui/material/Box/Box"; +import { observer } from "mobx-react-lite"; +import { consultingStore } from "../../stores/consulting"; +import { ConsultingWrapperProps } from "./ConsultingWrapper"; +import { makeStyles } from "@material-ui/core"; +import { PageSpinner } from "@postgres.ai/shared/components/PageSpinner"; +import { ProductCardWrapper } from "../../components/ProductCard/ProductCardWrapper"; +import { Link } from "@postgres.ai/shared/components/Link2"; +import Permissions from "../../utils/permissions"; +import { WarningWrapper } from "../../components/Warning/WarningWrapper"; +import { messages } from "../../assets/messages"; +import { ConsoleBreadcrumbsWrapper } from "../../components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper"; +import { formatPostgresInterval } from "./utils"; + + + +const useStyles = makeStyles((theme) => ({ + sectionLabel: { + fontSize: '14px!important', + fontWeight: '700!important' as 'bold', + }, + productCardProjects: { + flex: '1 1 0', + marginRight: '20px', + height: 'maxContent', + gap: 20, + maxHeight: '100%', + + '& svg': { + width: '206px', + height: '130px', + }, + + [theme.breakpoints.down('sm')]: { + flex: '100%', + marginTop: '20px', + minHeight: 'auto !important', + + '&:nth-child(1) svg': { + marginBottom: 0, + }, + + '&:nth-child(2) svg': { + marginBottom: 0, + }, + }, + }, +})) + +export const Consulting = observer((props: ConsultingWrapperProps) => { + const { orgId, orgData, match } = props; + + const classes = useStyles(); + + useEffect(() => { + if (orgId) { + consultingStore.getOrgBalance(orgId); + consultingStore.getTransactions(orgId); + } + }, [orgId]); + + const breadcrumbs = ( + + ) + + if (consultingStore.loading) { + return ( + + {breadcrumbs} + + + + + + ) + } + + if (orgData === null || !Permissions.isAdmin(orgData)) { + return ( + + {breadcrumbs} + + {messages.noPermissionPage} + + ) + } + + if (orgData.consulting_type === null) { + return ( + + {breadcrumbs} + + + Learn more) + } + ]} + > +

+ Your organization is not a consulting customer yet. To learn more about Postgres.AI consulting, visit this page: Consulting. +

+

+ Reach out to the team to discuss consulting opportunities: consulting@postgres.ai. +

+
+
+
+ ) + } + + return ( +
+ {breadcrumbs} + + + {orgData.consulting_type === 'retainer' && + + Retainer balance: + + + {formatPostgresInterval(consultingStore.orgBalance?.[0]?.balance || '00') || 0} + + } + + + + + + + + + Issue tracker (GitLab): + + + + https://gitlab.com/postgres-ai/postgresql-consulting/support/{orgData.alias} + + + + + + + + Book a Zoom call: + + + + https://calend.ly/postgres + + + + + + + Activity: + + { + consultingStore.transactions?.length === 0 + ? + No activity yet + + : +
+ + + Action + Amount + Date + Details + + + + { + consultingStore.transactions.map((transaction, index) => { + return ( + + {transaction.amount.charAt(0) === '-' ? 'Utilize' : 'Replenish'} + + {formatPostgresInterval(transaction.amount || '00')} + + {new Date(transaction.created_at)?.toISOString()?.split('T')?.[0]} + + {transaction.issue_id + ? + {transaction.description} + + : transaction.description + } + + + ); + }) + } + +
+ + } + + +
+ ); +}); \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Consulting/utils.ts b/ui/packages/platform/src/pages/Consulting/utils.ts new file mode 100644 index 00000000..361feae7 --- /dev/null +++ b/ui/packages/platform/src/pages/Consulting/utils.ts @@ -0,0 +1,30 @@ +import parse, { IPostgresInterval } from "postgres-interval" + +export function formatPostgresInterval(balance: string): string { + const interval: IPostgresInterval = parse(balance); + + const units: Partial, string>> = { + years: 'y', + months: 'mo', + days: 'd', + hours: 'h', + minutes: 'm', + seconds: 's', + milliseconds: 'ms', + }; + + const sign = Object.keys(units) + .map((key) => interval[key as keyof IPostgresInterval] || 0) + .find((value) => value !== 0) ?? 0; + + const isNegative = sign < 0; + + const formattedParts = (Object.keys(units) as (keyof typeof units)[]) + .map((key) => { + const value = interval[key]; + return value && Math.abs(value) > 0 ? `${Math.abs(value)}${units[key]}` : null; + }) + .filter(Boolean); + + return (isNegative ? '-' : '') + formattedParts.join(' '); +} \ No newline at end of file diff --git a/ui/packages/platform/src/stores/consulting.ts b/ui/packages/platform/src/stores/consulting.ts new file mode 100644 index 00000000..0fe13ed7 --- /dev/null +++ b/ui/packages/platform/src/stores/consulting.ts @@ -0,0 +1,101 @@ +import { makeAutoObservable, runInAction } from "mobx"; +import { request } from "../helpers/request"; + +const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; + +interface Transaction { + id: string; + org_id: number; + issue_id: number; + amount: string; + description?: string; + source: string; + created_at: string; +} + +interface OrgBalance { + org_id: number; + balance: string; +} + +class ConsultingStore { + orgBalance: OrgBalance[] | null = null; + transactions: Transaction[] = []; + loading: boolean = false; + error: string | null = null; + + constructor() { + makeAutoObservable(this); + } + + async getOrgBalance(orgId: number) { + this.loading = true; + this.error = null; + + try { + const response = await request(`${apiServer}/org_balance?org_id=eq.${orgId}`, { + method: "GET", + headers: { + + Prefer: "return=representation", + }, + }); + if (!response.ok) { + console.error(`Error: ${response.statusText}`); + } + + const data: OrgBalance[] = await response.json(); + runInAction(() => { + this.orgBalance = data; + }); + } catch (err: unknown) { + runInAction(() => { + if (err instanceof Error) { + this.error = err.message || "Failed to fetch org_balance"; + } else { + this.error = err as string; + } + }); + } finally { + runInAction(() => { + this.loading = false; + }); + } + } + + async getTransactions(orgId: number) { + this.loading = true; + this.error = null; + + try { + const response = await request(`${apiServer}/consulting_transactions?org_id=eq.${orgId}`, { + method: "GET", + headers: { + Prefer: "return=representation", + }, + }); + if (!response.ok) { + console.error(`Error: ${response.statusText}`); + } + + const data: Transaction[] = await response.json(); + runInAction(() => { + this.transactions = data; + }); + } catch (err: unknown) { + runInAction(() => { + if (err instanceof Error) { + this.error = err.message || "Failed to fetch transactions"; + } else { + this.error = err as string; + } + }); + } finally { + runInAction(() => { + this.loading = false; + }); + } + } +} + +export const consultingStore = new ConsultingStore(); \ No newline at end of file diff --git a/ui/packages/shared/styles/icons.tsx b/ui/packages/shared/styles/icons.tsx index d1d52fc5..b8dfbff5 100644 --- a/ui/packages/shared/styles/icons.tsx +++ b/ui/packages/shared/styles/icons.tsx @@ -1907,5 +1907,11 @@ export const icons = { d="m384 85.3333337 85.333333 85.3333333v256H42.66666678L42.66525 193.996358c10.0983011 15.352321 24.2153849 33.106855 42.6673443 48.165701L85.3333334 384H426.666667V181.333334l-53.333334-53.3333337-39.735846.0017872c-5.439498-10.6533523-14.584184-26.4898523-27.734229-42.6683963zM384 320v21.333334H128.0000001V320zm0-64v21.333334H256l-.000063-20.370657c.541196-.318106 1.079687-.63898 1.615477-.962551zM181.333333 42.666667C278.4 42.666667 320 149.333334 320 149.333334S278.4 256 181.333333 256C84.2666668 256 42.66666678 149.333334 42.66666678 149.333334S84.2666668 42.666667 181.333333 42.666667zm0 26.6666667c-61.2906662 0-97.0666662 57.0666666-108.2986662 80.0000003 11.232 22.933333 47.008 80 108.2986662 80 61.290667 0 97.066667-57.066667 108.298667-80-11.232-22.9333337-47.008-80.0000003-108.298667-80.0000003zm0 33.3333333c26.80422 0 48.533334 20.8933783 48.533334 46.666667 0 25.773288-21.729114 46.666666-48.533334 46.666666-26.804219 0-48.5333329-20.893378-48.5333329-46.666666 0-25.7732887 21.7291139-46.666667 48.5333329-46.666667zm0 26.6666667c-11.487522 0-20.8 8.954305-20.8 20.0000003 0 11.045695 9.312478 20 20.8 20 11.487523 0 20.8-8.954305 20.8-20 0-11.0456953-9.312477-20.0000003-20.8-20.0000003z" /> + ), + consultingIcon: ( + + + ) } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index db0ffb93..005f5987 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -378,6 +378,9 @@ importers: moment: specifier: '>=2.29.2' version: 2.30.1 + postgres-interval: + specifier: ^4.0.2 + version: 4.0.2 prop-types: specifier: ^15.7.2 version: 15.8.1 @@ -12307,6 +12310,11 @@ packages: picocolors: 1.0.0 source-map-js: 1.2.0 + /postgres-interval@4.0.2: + resolution: {integrity: sha512-EMsphSQ1YkQqKZL2cuG0zHkmjCCzQqQ71l2GXITqRwjhRleCdv00bDk/ktaSi0LnlaPzAc3535KTrjXsTdtx7A==} + engines: {node: '>=12'} + dev: false + /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} From 5be5369f1281da3741718d66b1b14942bce6ac29 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 30 Jan 2025 22:13:47 +0000 Subject: [PATCH 103/111] fix(ui): Move audit logs into Manage menu + small screen layout fix --- .../AuditSettingsForm/AuditSettingsForm.tsx | 5 ++- .../src/components/IndexPage/IndexPage.tsx | 41 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx index a952f1bd..5d3538f3 100644 --- a/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx +++ b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx @@ -91,6 +91,9 @@ const useStyles = makeStyles( 'margin-top': '20px', }, }, + formContainer: { + flexWrap: 'nowrap' + }, textField: { ...styles.inputField, }, @@ -304,7 +307,7 @@ const AuditSettingsForm: React.FC = (props) => {
- + {!isAuditLogsSettingsAvailable && Become an Enterprise customer diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index 6d848ffe..7a6ad1d3 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -602,27 +602,6 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { - {orgPermissions && orgPermissions.auditLogView && ( - - - {icons.auditLogIcon} - - Audit - - )} )} + {orgPermissions && orgPermissions.auditLogView && ( + + + Audit logs + + + )} {orgData !== null && orgPermissions && Permissions.isAdmin(orgData) && orgPermissions.auditLogView && ( Date: Fri, 31 Jan 2025 19:43:35 +0000 Subject: [PATCH 104/111] Consulting section UI polishing --- .../TransactionsTable/TransactionsTable.tsx | 42 +++++++ .../platform/src/pages/Consulting/index.tsx | 109 +++++++----------- ui/packages/platform/src/stores/consulting.ts | 2 +- 3 files changed, 84 insertions(+), 69 deletions(-) create mode 100644 ui/packages/platform/src/pages/Consulting/TransactionsTable/TransactionsTable.tsx diff --git a/ui/packages/platform/src/pages/Consulting/TransactionsTable/TransactionsTable.tsx b/ui/packages/platform/src/pages/Consulting/TransactionsTable/TransactionsTable.tsx new file mode 100644 index 00000000..15e112ef --- /dev/null +++ b/ui/packages/platform/src/pages/Consulting/TransactionsTable/TransactionsTable.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material"; +import { Transaction } from "stores/consulting"; +import { formatPostgresInterval } from "../utils"; +import { Link } from "@postgres.ai/shared/components/Link2"; + + +type TransactionsTableProps = { + transactions: Transaction[], + alias: string +} + +export const TransactionsTable = ({ transactions, alias }: TransactionsTableProps) => ( + + + + + Action + Amount + Date + Details + + + + {transactions.map(({ amount, created_at, issue_id, description, id }: Transaction) => ( + + {amount.charAt(0) === '-' ? 'Utilize' : 'Replenish'} + {formatPostgresInterval(amount || '00')} + {new Date(created_at)?.toISOString()?.split('T')?.[0]} + + {issue_id ? ( + + {description} + + ) : description} + + + ))} + +
+
+); \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Consulting/index.tsx b/ui/packages/platform/src/pages/Consulting/index.tsx index fc5bb9b7..1f04f753 100644 --- a/ui/packages/platform/src/pages/Consulting/index.tsx +++ b/ui/packages/platform/src/pages/Consulting/index.tsx @@ -1,12 +1,7 @@ import React, { useEffect } from "react"; import ConsolePageTitle from "../../components/ConsolePageTitle"; -import Table from '@mui/material/Table'; -import TableBody from '@mui/material/TableBody'; -import TableCell from '@mui/material/TableCell'; -import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; -import TableRow from '@mui/material/TableRow'; -import { Grid, Paper, Typography } from "@mui/material"; +import Alert from "@mui/material/Alert"; +import { Grid, Typography } from "@mui/material"; import Button from "@mui/material/Button"; import Box from "@mui/material/Box/Box"; import { observer } from "mobx-react-lite"; @@ -16,11 +11,9 @@ import { makeStyles } from "@material-ui/core"; import { PageSpinner } from "@postgres.ai/shared/components/PageSpinner"; import { ProductCardWrapper } from "../../components/ProductCard/ProductCardWrapper"; import { Link } from "@postgres.ai/shared/components/Link2"; -import Permissions from "../../utils/permissions"; -import { WarningWrapper } from "../../components/Warning/WarningWrapper"; -import { messages } from "../../assets/messages"; import { ConsoleBreadcrumbsWrapper } from "../../components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper"; import { formatPostgresInterval } from "./utils"; +import { TransactionsTable } from "./TransactionsTable/TransactionsTable"; @@ -88,16 +81,6 @@ export const Consulting = observer((props: ConsultingWrapperProps) => { ) } - if (orgData === null || !Permissions.isAdmin(orgData)) { - return ( - - {breadcrumbs} - - {messages.noPermissionPage} - - ) - } - if (orgData.consulting_type === null) { return ( @@ -132,19 +115,11 @@ export const Consulting = observer((props: ConsultingWrapperProps) => { {breadcrumbs} - {orgData.consulting_type === 'retainer' && - - Retainer balance: - - - {formatPostgresInterval(consultingStore.orgBalance?.[0]?.balance || '00') || 0} - - } - + + Thank you for choosing Postgres.AI as your PostgreSQL consulting partner. Your plan: {orgData.consulting_type.toUpperCase()}. + @@ -172,6 +147,37 @@ export const Consulting = observer((props: ConsultingWrapperProps) => {
+ + + Email: + + + + consulting@postgres.ai + + + + + {consultingStore.orgBalance?.[0]?.balance?.charAt(0) === '-' && + Consulting hours overdrawn + } + {orgData.consulting_type === 'retainer' && + + Retainer balance: + + + {formatPostgresInterval(consultingStore.orgBalance?.[0]?.balance || '00') || 0} + + } + {orgData.consulting_type === 'retainer' && + + + + } + + {orgData.consulting_type === 'retainer' && Activity: @@ -180,44 +186,11 @@ export const Consulting = observer((props: ConsultingWrapperProps) => { ? No activity yet - : - - - - Action - Amount - Date - Details - - - - { - consultingStore.transactions.map((transaction, index) => { - return ( - - {transaction.amount.charAt(0) === '-' ? 'Utilize' : 'Replenish'} - - {formatPostgresInterval(transaction.amount || '00')} - - {new Date(transaction.created_at)?.toISOString()?.split('T')?.[0]} - - {transaction.issue_id - ? - {transaction.description} - - : transaction.description - } - - - ); - }) - } - -
-
+ : } -
+
}
); -}); \ No newline at end of file +}); + diff --git a/ui/packages/platform/src/stores/consulting.ts b/ui/packages/platform/src/stores/consulting.ts index 0fe13ed7..2eb197ba 100644 --- a/ui/packages/platform/src/stores/consulting.ts +++ b/ui/packages/platform/src/stores/consulting.ts @@ -3,7 +3,7 @@ import { request } from "../helpers/request"; const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; -interface Transaction { +export interface Transaction { id: string; org_id: number; issue_id: number; From b42bd7cf4b4a20163fb156137d87aebacb7915a7 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Tue, 25 Feb 2025 16:32:59 +0000 Subject: [PATCH 105/111] feat(ui): Using unified dblab_clone API endpoint --- ui/packages/platform/src/api/clones/createClone.ts | 8 +++++--- ui/packages/platform/src/api/clones/destroyClone.ts | 5 +++-- ui/packages/platform/src/api/clones/getClone.ts | 9 +++++---- ui/packages/platform/src/api/clones/resetClone.ts | 7 ++++--- ui/packages/platform/src/api/clones/updateClone.ts | 7 ++++--- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/ui/packages/platform/src/api/clones/createClone.ts b/ui/packages/platform/src/api/clones/createClone.ts index 8a8b1f8b..6fbc7666 100644 --- a/ui/packages/platform/src/api/clones/createClone.ts +++ b/ui/packages/platform/src/api/clones/createClone.ts @@ -12,11 +12,13 @@ type Req = { } export const createClone = async (req: Req) => { - const response = await request('/rpc/dblab_clone_create', { + const response = await request('/rpc/dblab_api_call', { method: 'POST', body: JSON.stringify({ instance_id: req.instanceId, - clone_data: { + action: '/clone', + method: 'post', + data: { id: req.cloneId, snapshot: { id: req.snapshotId, @@ -27,7 +29,7 @@ export const createClone = async (req: Req) => { }, protected: req.isProtected, }, - }), + }) }) return { diff --git a/ui/packages/platform/src/api/clones/destroyClone.ts b/ui/packages/platform/src/api/clones/destroyClone.ts index 96ebae5b..40642639 100644 --- a/ui/packages/platform/src/api/clones/destroyClone.ts +++ b/ui/packages/platform/src/api/clones/destroyClone.ts @@ -10,11 +10,12 @@ import { DestroyClone } from '@postgres.ai/shared/types/api/endpoints/destroyClo import { request } from 'helpers/request' export const destroyClone: DestroyClone = async (req) => { - const response = await request('/rpc/dblab_clone_destroy', { + const response = await request('/rpc/dblab_api_call', { method: 'POST', body: JSON.stringify({ + action: '/clone/' + encodeURIComponent(req.cloneId), instance_id: req.instanceId, - clone_id: req.cloneId, + method: 'delete' }), }) diff --git a/ui/packages/platform/src/api/clones/getClone.ts b/ui/packages/platform/src/api/clones/getClone.ts index 067c7a96..3534e426 100644 --- a/ui/packages/platform/src/api/clones/getClone.ts +++ b/ui/packages/platform/src/api/clones/getClone.ts @@ -11,17 +11,18 @@ type Request = { } export const getClone = async (req: Request) => { - const response = (await request('/rpc/dblab_clone_status', { + const response = (await request('/rpc/dblab_api_call', { method: 'POST', body: JSON.stringify({ + action: '/clone/' + encodeURIComponent(req.cloneId), instance_id: req.instanceId, - clone_id: req.cloneId, - }), + method: 'get' + }) })) return { response: response.ok - ? formatCloneDto((await response.json()) as CloneDto) + ? formatCloneDto(await response.json() as CloneDto) : null, error: response.ok ? null : response, } diff --git a/ui/packages/platform/src/api/clones/resetClone.ts b/ui/packages/platform/src/api/clones/resetClone.ts index 4feaebbd..0b09fe94 100644 --- a/ui/packages/platform/src/api/clones/resetClone.ts +++ b/ui/packages/platform/src/api/clones/resetClone.ts @@ -10,12 +10,13 @@ import { ResetClone } from '@postgres.ai/shared/types/api/endpoints/resetClone' import { request } from 'helpers/request' export const resetClone: ResetClone = async (req) => { - const response = await request('/rpc/dblab_clone_reset', { + const response = await request('/rpc/dblab_api_call', { method: 'post', body: JSON.stringify({ + action: '/clone/' + encodeURIComponent(req.cloneId) + '/reset', instance_id: req.instanceId, - clone_id: req.cloneId, - reset_options: { + method: 'post', + data: { snapshotID: req.snapshotId, latest: false, }, diff --git a/ui/packages/platform/src/api/clones/updateClone.ts b/ui/packages/platform/src/api/clones/updateClone.ts index fb61b1ae..a28b4870 100644 --- a/ui/packages/platform/src/api/clones/updateClone.ts +++ b/ui/packages/platform/src/api/clones/updateClone.ts @@ -3,12 +3,13 @@ import { UpdateClone } from '@postgres.ai/shared/types/api/endpoints/updateClone import { request } from 'helpers/request' export const updateClone: UpdateClone = async (req) => { - const response = await request('/rpc/dblab_clone_update', { + const response = await request('/rpc/dblab_api_call', { method: 'POST', body: JSON.stringify({ + action: '/clone/' + encodeURIComponent(req.cloneId), instance_id: req.instanceId, - clone_id: req.cloneId, - clone: { + method: 'patch', + data: { protected: req.clone.isProtected, }, }), From ca1d6a8508a8955af4738ac3747d046194bccc83 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Tue, 25 Feb 2025 17:25:25 +0000 Subject: [PATCH 106/111] Add cypress install step to e2e test CI job --- ui/.gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/.gitlab-ci.yml b/ui/.gitlab-ci.yml index b101c8a6..c54ee265 100644 --- a/ui/.gitlab-ci.yml +++ b/ui/.gitlab-ci.yml @@ -73,5 +73,6 @@ e2e-ce-ui-test: # - pnpm config set store-dir /builds/postgres-ai/database-lab/.pnpm-store/ script: - pnpm --dir ui/ i --no-frozen-lockfile + - pnpm --dir ui/ --filter @postgres.ai/ce exec cypress install - pnpm --dir ui/ --filter @postgres.ai/ce start & wait-on http://localhost:3001 - pnpm --dir ui/ --filter @postgres.ai/ce cy:run From 4a7e6b0a35a750b7666c4980f223e70940c7b922 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Mon, 17 Mar 2025 02:44:12 +0000 Subject: [PATCH 107/111] feat (ui): Add LLM model selector, remove model selection from chat settings & small UI improvements on small screens --- .../src/pages/Bot/ChatsList/ChatsList.tsx | 7 +- .../pages/Bot/ModelSelector/ModelSelector.tsx | 81 +++++++++++++++++ .../Bot/SettingsDialog/SettingsDialog.tsx | 90 +++++++++---------- .../pages/Bot/SettingsPanel/SettingsPanel.tsx | 74 ++++++--------- ui/packages/platform/src/pages/Bot/index.tsx | 16 ++-- 5 files changed, 165 insertions(+), 103 deletions(-) create mode 100644 ui/packages/platform/src/pages/Bot/ModelSelector/ModelSelector.tsx diff --git a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx index 4d990e6a..54ea0fec 100644 --- a/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx +++ b/ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx @@ -31,7 +31,7 @@ const useStyles = makeStyles((theme) => ({ [theme.breakpoints.down('sm')]: { height: '100vh!important', marginTop: '0!important', - width: 320, + width: 'min(100%, 360px)', zIndex: 9999 }, '& > ul': { @@ -57,6 +57,11 @@ const useStyles = makeStyles((theme) => ({ background: 'white', [theme.breakpoints.down('sm')]: { padding: 0 + }, + "@media (max-width: 960px)": { + "& .MuiFormControl-root": { + display: "none" // Hide model selector in chats list + } } }, listItemLink: { diff --git a/ui/packages/platform/src/pages/Bot/ModelSelector/ModelSelector.tsx b/ui/packages/platform/src/pages/Bot/ModelSelector/ModelSelector.tsx new file mode 100644 index 00000000..2b26eda0 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/ModelSelector/ModelSelector.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { FormControl, Select, MenuItem, Typography, InputLabel, useMediaQuery } from "@mui/material"; +import { SelectChangeEvent } from "@mui/material/Select"; + +import { useAiBot } from "../hooks"; + +export const ModelSelector = () => { + const { aiModel, aiModels, setAiModel } = useAiBot(); + const isSmallScreen = useMediaQuery("(max-width: 960px)"); + + const handleChange = (event: SelectChangeEvent) => { + const [vendor, name] = (event.target.value as string).split("/"); + const model = aiModels?.find( + (model) => model.vendor === vendor && model.name === name + ); + if (model) setAiModel(model); + }; + + const truncateText = (text: string, maxLength: number) => { + return text.length > maxLength ? text.substring(0, maxLength) + "..." : text; + }; + + return ( + + + + ); +}; diff --git a/ui/packages/platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx b/ui/packages/platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx index d34957dc..6a5dcab4 100644 --- a/ui/packages/platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx +++ b/ui/packages/platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx @@ -15,7 +15,7 @@ import { makeStyles, Radio, RadioGroup, - TextField, + TextField, Theme, Typography, } from '@material-ui/core' import MuiDialogTitle from '@material-ui/core/DialogTitle' @@ -30,6 +30,8 @@ import { AiModel } from "../../../types/api/entities/bot"; import settings from "../../../utils/settings"; import { Link } from "@postgres.ai/shared/components/Link2"; import { ExternalIcon } from "@postgres.ai/shared/icons/External"; +import Divider from "@material-ui/core/Divider"; +import cn from "classnames"; type DialogTitleProps = { id: string @@ -123,35 +125,30 @@ const DialogActions = (props: { children: React.ReactNode }) => { ) } -const useDialogStyles = makeStyles( - () => ({ +const useDialogStyles = makeStyles( + (theme) => ({ textField: { ...styles.inputField, marginTop: '0px', width: 480, + [theme.breakpoints.down('sm')]: { + + } }, copyButton: { marginTop: '-3px', fontSize: '20px', }, - dialog: {}, - remark: { - fontSize: 12, - lineHeight: '12px', - - paddingLeft: 20, - paddingBottom: 5, - }, - remarkIcon: { - display: 'block', - height: '20px', - width: '22px', - float: 'left', - paddingTop: '5px', - }, urlContainer: { - marginTop: 10, - paddingLeft: 22, + marginTop: 8, + paddingLeft: 20, + [theme.breakpoints.down('sm')]: { + padding: 0, + width: '100%', + '& .MuiTextField-root': { + maxWidth: 'calc(100% - 36px)' + } + }, }, radioGroup: { fontSize: 12, @@ -170,9 +167,24 @@ const useDialogStyles = makeStyles( marginBottom: 0 } }, + unlockNoteDemo: { + paddingLeft: 20 + }, formControlLabel: { '& .Mui-disabled > *, & .Mui-disabled': { color: 'rgba(0, 0, 0, 0.6)' + }, + [theme.breakpoints.down('sm')]: { + marginRight: 0, + alignItems: 'flex-start', + '&:first-child': { + marginTop: 6 + } + }, + }, + formControlLabelRadio: { + [theme.breakpoints.down('sm')]: { + padding: '4px 9px' } }, externalIcon: { @@ -180,6 +192,9 @@ const useDialogStyles = makeStyles( height: 14, marginLeft: 4, transform: 'translateY(2px)', + }, + divider: { + margin: '12px 0' } }), { index: 1 }, @@ -295,8 +310,8 @@ export const SettingsDialog = (props: PublicChatDialogProps) => { <> Visibility { setVisibility(event.target.value as Visibility) @@ -306,8 +321,9 @@ export const SettingsDialog = (props: PublicChatDialogProps) => { } + control={} label={<>Public: anyone can view chats, but only team members can respond} + aria-label="Public: anyone can view chats, but only team members can respond" /> {visibility === Visibility.PUBLIC && threadId && (
{urlField}
@@ -315,11 +331,12 @@ export const SettingsDialog = (props: PublicChatDialogProps) => { } + control={} label={<>Private: chats are visible only to members of your organization} + aria-label="Private: chats are visible only to members of your organization" disabled={Boolean(isDemoOrg) || !isSubscriber} /> - {Boolean(isDemoOrg) && Private chats are not allowed in "Demo"} + {Boolean(isDemoOrg) && Private chats are not allowed in "Demo"} {!Boolean(isDemoOrg) && !isSubscriber && Unlock private conversations by either:
    @@ -339,29 +356,6 @@ export const SettingsDialog = (props: PublicChatDialogProps) => { } - {aiModels && <> - Model - { - const selectedModel = aiModels?.find((model) => `${model.vendor}/${model.name}` === event.target.value) - setModel(selectedModel!) - }} - className={classes.radioGroup} - > - {aiModels.map((model) => - } - label={`${model.name} ${model.comment ? model.comment : ''}`} - /> - ) - } - - } diff --git a/ui/packages/platform/src/pages/Bot/SettingsPanel/SettingsPanel.tsx b/ui/packages/platform/src/pages/Bot/SettingsPanel/SettingsPanel.tsx index b457841b..27bb931d 100644 --- a/ui/packages/platform/src/pages/Bot/SettingsPanel/SettingsPanel.tsx +++ b/ui/packages/platform/src/pages/Bot/SettingsPanel/SettingsPanel.tsx @@ -7,6 +7,8 @@ import { theme } from "@postgres.ai/shared/styles/theme"; import { permalinkLinkBuilder } from "../utils"; import { useAiBot } from "../hooks"; import DeveloperModeIcon from "@material-ui/icons/DeveloperMode"; +import { ModelSelector } from "../ModelSelector/ModelSelector"; +import { Skeleton } from "@mui/material"; export type SettingsPanelProps = { onSettingsClick: () => void; @@ -31,25 +33,14 @@ const useStyles = makeStyles((theme) => ({ } }, labelVisibility: { - marginLeft: '0.5rem', + marginRight: '0.5rem', [theme.breakpoints.down('sm')]: { - marginLeft: '0.25rem' + marginRight: '0.25rem' }, '&:hover': { backgroundColor: colors.secondary1.main } }, - labelModel: { - background: colors.secondary1.main, - }, - labelModelInvalid: { - background: colors.state.error, - border: "none", - cursor: 'pointer', - '&:hover': { - backgroundColor: colors.primary.dark - } - }, labelPrivate: { backgroundColor: colors.pgaiDarkGray, }, @@ -74,48 +65,33 @@ const useStyles = makeStyles((theme) => ({ export const SettingsPanel = (props: SettingsPanelProps) => { const { onSettingsClick, onConsoleClick } = props; + const { loading } = useAiBot() const classes = useStyles(); const matches = useMediaQuery(theme.breakpoints.down('sm')); - const { messages, chatVisibility, aiModel, aiModelsLoading } = useAiBot(); + const { messages, chatVisibility, aiModelsLoading } = useAiBot(); const permalinkId = useMemo(() => messages?.[0]?.id, [messages]); - let modelLabel; - - if (aiModel) { - modelLabel = ( - - {aiModel.name} - - ) - } else { - modelLabel = ( - - ) - } - return ( <> - {!aiModelsLoading && modelLabel} - {permalinkId && - {chatVisibility} thread - } + {permalinkId && <> + {loading + ? + : + {chatVisibility} thread + + } + } + {!aiModelsLoading && } + + + + + + ); +}; + +export default DBLabSettingsForm diff --git a/ui/packages/platform/src/components/DBLabSettingsForm/DBLabSettingsFormWrapper.tsx b/ui/packages/platform/src/components/DBLabSettingsForm/DBLabSettingsFormWrapper.tsx new file mode 100644 index 00000000..e9fd4075 --- /dev/null +++ b/ui/packages/platform/src/components/DBLabSettingsForm/DBLabSettingsFormWrapper.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import DBLabSettingsForm from "./DBLabSettingsForm"; + +export interface DBLabSettingsFormProps { + mode?: string | undefined + project?: string | undefined + org?: string | number + orgId?: number + orgPermissions?: { + settingsOrganizationUpdate?: boolean + } + orgData?: { + priveleged_until: Date + chats_private_allowed: boolean + consulting_type: string | null + } + match: { + params: { + project?: string + projectId?: string | number | undefined + org?: string + } + } +} + + + +export const DBLabSettingsFormWrapper = (props: DBLabSettingsFormProps) => { + return +} diff --git a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx index 1a90dc38..fcc48832 100644 --- a/ui/packages/platform/src/components/IndexPage/IndexPage.tsx +++ b/ui/packages/platform/src/components/IndexPage/IndexPage.tsx @@ -6,7 +6,7 @@ */ import React, { Component, useState } from 'react' -import { Switch, Route, NavLink, Redirect, useRouteMatch } from 'react-router-dom' +import { Switch, Route, NavLink, Redirect } from 'react-router-dom' import { AppBar, Toolbar, @@ -90,6 +90,7 @@ import cn from "classnames"; import { BotSettingsFormWrapper } from "../BotSettingsForm/BotSettingsFormWrapper"; import { AuditSettingsFormWrapper } from "../AuditSettingsForm/AuditSettingsFormWrapper"; import { ExpandLess, ExpandMore } from "@material-ui/icons"; +import { DBLabSettingsFormWrapper } from "../DBLabSettingsForm/DBLabSettingsFormWrapper"; interface IndexPageWithStylesProps extends IndexPageProps { @@ -725,6 +726,21 @@ function OrganizationMenu(parentProps: OrganizationMenuProps) { )} + {orgData !== null && Permissions.isAdmin(orgData) && ( + + + DBLab settings + + + )} {orgPermissions && orgPermissions.auditLogView && ( )} /> + ( + + )} + /> ( diff --git a/ui/packages/platform/src/components/types/index.ts b/ui/packages/platform/src/components/types/index.ts index cb434205..61ff39b3 100644 --- a/ui/packages/platform/src/components/types/index.ts +++ b/ui/packages/platform/src/components/types/index.ts @@ -41,6 +41,8 @@ export interface Orgs { is_chat_public_by_default: boolean chats_private_allowed: boolean consulting_type: string | null + dblab_old_clones_notifications_threshold_hours: number | null + dblab_low_disk_space_notifications_threshold_percent: number | null data: { plan: string } | null diff --git a/ui/packages/platform/src/pages/Profile/ProfileWrapper.tsx b/ui/packages/platform/src/pages/Profile/ProfileWrapper.tsx index 6f94a199..bca6b13b 100644 --- a/ui/packages/platform/src/pages/Profile/ProfileWrapper.tsx +++ b/ui/packages/platform/src/pages/Profile/ProfileWrapper.tsx @@ -39,6 +39,14 @@ export const ProfileWrapper = () => { color: '#000!important', fontWeight: 'bold', }, + subLabel: { + marginTop: theme.spacing(2), + marginLeft: theme.spacing(1), + marginBottom: theme.spacing(1), + color: '#000!important', + fontWeight: 500, + width: '100%' + }, dense: { marginTop: 16, }, diff --git a/ui/packages/platform/src/pages/Profile/index.jsx b/ui/packages/platform/src/pages/Profile/index.jsx index 1a95715c..a1529059 100644 --- a/ui/packages/platform/src/pages/Profile/index.jsx +++ b/ui/packages/platform/src/pages/Profile/index.jsx @@ -68,6 +68,8 @@ class Profile extends Component { is_chats_email_notifications_enabled: values.is_chats_email_notifications_enabled, first_name: values.first_name, last_name: values.last_name, + dblab_low_disk_space_notifications_enabled: values.is_dblab_low_disk_space_notifications_enabled, + dblab_old_clones_notifications_enabled: values.is_dblab_old_clones_notifications_enabled }); } }; @@ -80,6 +82,8 @@ class Profile extends Component { first_name: data?.data?.info?.first_name || '', last_name: data?.data?.info?.last_name || '', is_chats_email_notifications_enabled: data?.data?.info?.chats_email_notifications_enabled || false, + is_dblab_low_disk_space_notifications_enabled: data?.data?.info?.dblab_low_disk_space_notifications_enabled, + is_dblab_old_clones_notifications_enabled: data?.data?.info?.dblab_old_clones_notifications_enabled }; @@ -182,6 +186,41 @@ class Profile extends Component { } label="Send an email notification if a new message from AI Assistant remains unread for more than one minute" /> + + DBLab notifications + + } + checkedIcon={} + name="is_dblab_low_disk_space_notifications_enabled" + className={classes.formControlLabelCheckbox} + checked={values.is_dblab_low_disk_space_notifications_enabled} + onChange={(event) => + setFieldValue('is_dblab_low_disk_space_notifications_enabled', event.target.checked) + } + /> + } + label="Receive notifications about low disk space" //@TODO: @Nik, help me with text here, I think it should be connected with "Administrators" role in org + /> + } + checkedIcon={} + name="is_dblab_old_clones_notifications_enabled" + className={classes.formControlLabelCheckbox} + checked={values.is_dblab_old_clones_notifications_enabled} + onChange={(event) => + setFieldValue('is_dblab_old_clones_notifications_enabled', event.target.checked) + } + /> + } + label="Receive notifications about old clones" + /> 0) { + this.data.orgProfile.updateErrorFields = null; + this.data.orgProfile.data = data[0]; + Actions.getUserProfile(this.data.auth.token); + Actions.getOrgs(this.data.auth.token, this.data.orgProfile.orgId); + Actions.showNotification('DBLab settings successfully saved.', 'success'); + } + + this.trigger(this.data); + }, + onTestSiemServiceConnectionFailed: function (error) { this.data.orgProfile.isUpdating = false; this.trigger(this.data); diff --git a/ui/packages/platform/src/utils/utils.ts b/ui/packages/platform/src/utils/utils.ts index 8707bef6..57c9285c 100644 --- a/ui/packages/platform/src/utils/utils.ts +++ b/ui/packages/platform/src/utils/utils.ts @@ -5,6 +5,8 @@ *-------------------------------------------------------------------------- */ +import parse, { IPostgresInterval } from "postgres-interval"; + export const generateToken = function () { const a = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'.split('') @@ -68,4 +70,35 @@ export const isMobileDevice = (): boolean => { const isMobileScreen = window.innerWidth <= 1366; return hasTouchScreen && isMobileScreen; +} + +export const pgIntervalToHours = (interval?: string | null): number | null => { + if (!interval) { + return null; + } + + const parsed: IPostgresInterval = parse(interval); + + const yearsToHours = (parsed.years ?? 0) * 365 * 24; + const monthsToHours = (parsed.months ?? 0) * 30 * 24; + const daysToHours = (parsed.days ?? 0) * 24; + const hours = parsed.hours ?? 0; + const minutesToHours = (parsed.minutes ?? 0) / 60; + const secondsToHours = (parsed.seconds ?? 0) / 3600; + + return yearsToHours + monthsToHours + daysToHours + hours + minutesToHours + secondsToHours; +} + +export const hoursToPgInterval = (hours: number): string => { + const totalMinutes = Math.floor(hours * 60); + const days = Math.floor(totalMinutes / (24 * 60)); + const remainingMinutes = totalMinutes % (24 * 60); + const h = Math.floor(remainingMinutes / 60); + const m = remainingMinutes % 60; + + if (days > 0) { + return `${days} days ${h}:${m}:00`; + } else { + return `${h}:${m}:00`; + } } \ No newline at end of file From 2fd56d7c39c4944e155da512321a7e4589bcb7e7 Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Thu, 20 Mar 2025 16:03:51 +0000 Subject: [PATCH 109/111] Tool_calls and Thinking blocks folding --- ui/cspell.json | 4 +- .../Message/{ => CodeBlock}/CodeBlock.tsx | 2 +- .../{ => MermaidDiagram}/MermaidDiagram.tsx | 0 .../MermaidDiagramControls.tsx | 0 .../pages/Bot/Messages/Message/Message.tsx | 103 ++++++-------- .../Message/MessageHeader/MessageHeader.tsx | 118 ++++++++++++++++ .../Message/ThinkingCard/ThinkingCard.tsx | 66 +++++++++ .../ToolCallRenderer/ToolCallRenderer.tsx | 74 ++++++++++ .../src/pages/Bot/Messages/Messages.tsx | 1 + .../Sources/SourceCard/SourceCard.tsx | 126 ++++++++++++++++++ .../Bot/Messages/Sources/SourcesFullList.tsx | 81 +++++++++++ .../Bot/Messages/Sources/SourcesShortList.tsx | 58 ++++++++ .../platform/src/pages/Bot/Messages/utils.ts | 110 ++++++++++++++- ui/packages/platform/src/pages/Bot/hooks.tsx | 40 +++++- .../platform/src/types/api/entities/bot.ts | 22 ++- 15 files changed, 726 insertions(+), 79 deletions(-) rename ui/packages/platform/src/pages/Bot/Messages/Message/{ => CodeBlock}/CodeBlock.tsx (98%) rename ui/packages/platform/src/pages/Bot/Messages/Message/{ => MermaidDiagram}/MermaidDiagram.tsx (100%) rename ui/packages/platform/src/pages/Bot/Messages/Message/{ => MermaidDiagram}/MermaidDiagramControls.tsx (100%) create mode 100644 ui/packages/platform/src/pages/Bot/Messages/Message/MessageHeader/MessageHeader.tsx create mode 100644 ui/packages/platform/src/pages/Bot/Messages/Message/ThinkingCard/ThinkingCard.tsx create mode 100644 ui/packages/platform/src/pages/Bot/Messages/Message/ToolCallRenderer/ToolCallRenderer.tsx create mode 100644 ui/packages/platform/src/pages/Bot/Messages/Sources/SourceCard/SourceCard.tsx create mode 100644 ui/packages/platform/src/pages/Bot/Messages/Sources/SourcesFullList.tsx create mode 100644 ui/packages/platform/src/pages/Bot/Messages/Sources/SourcesShortList.tsx diff --git a/ui/cspell.json b/ui/cspell.json index 6cc3ab15..64382e04 100644 --- a/ui/cspell.json +++ b/ui/cspell.json @@ -202,6 +202,8 @@ "SPARQL", "subtransactions", "mbox", - "SIEM" + "SIEM", + "toolcall", + "thinkblock" ] } diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock/CodeBlock.tsx similarity index 98% rename from ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx rename to ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock/CodeBlock.tsx index ae7a21c3..564c21a9 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock/CodeBlock.tsx @@ -7,7 +7,7 @@ import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined'; import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'; import CodeIcon from '@material-ui/icons/Code'; -import { formatLanguageName } from "../../utils"; +import { formatLanguageName } from "../../../utils"; const useStyles = makeStyles((theme) => ({ container: { diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram/MermaidDiagram.tsx similarity index 100% rename from ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram.tsx rename to ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram/MermaidDiagram.tsx diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagramControls.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram/MermaidDiagramControls.tsx similarity index 100% rename from ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagramControls.tsx rename to ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram/MermaidDiagramControls.tsx diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx index 7a7c6a2d..3f58cac7 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' -import cn from "classnames"; import ReactMarkdown, { Components } from "react-markdown"; import rehypeRaw from "rehype-raw"; import remarkGfm from "remark-gfm"; @@ -7,14 +6,18 @@ import { makeStyles } from "@material-ui/core"; import { colors } from "@postgres.ai/shared/styles/colors"; import { icons } from "@postgres.ai/shared/styles/icons"; import { DebugDialog } from "../../DebugDialog/DebugDialog"; -import { CodeBlock } from "./CodeBlock"; -import { disallowedHtmlTagsForMarkdown, permalinkLinkBuilder } from "../../utils"; +import { CodeBlock } from "./CodeBlock/CodeBlock"; +import { disallowedHtmlTagsForMarkdown } from "../../utils"; import { MessageStatus, StateMessage } from "../../../../types/api/entities/bot"; -import { MermaidDiagram } from "./MermaidDiagram"; +import { MermaidDiagram } from "./MermaidDiagram/MermaidDiagram"; import { useAiBot } from "../../hooks"; +import { ToolCallRenderer } from "./ToolCallRenderer/ToolCallRenderer"; +import { transformAllCustomTags } from "../utils"; +import { ThinkBlockRenderer } from './ThinkingCard/ThinkingCard'; +import { MessageHeader } from "./MessageHeader/MessageHeader"; -type BaseMessageProps = { +export type BaseMessageProps = { id: string | null; created_at?: string; content?: string; @@ -249,7 +252,6 @@ const useStyles = makeStyles( '50%': { borderRightColor: 'black' }, }, }), - ) export const Message = React.memo((props: MessageProps) => { @@ -302,12 +304,16 @@ export const Message = React.memo((props: MessageProps) => { }; }, [id, updateMessageStatus, isCurrentStreamMessage, isAi, threadId, status]); - const contentToRender: string = content?.replace(/\n/g, ' \n') || '' + const contentToRender = useMemo(() => { + if (!content) return ''; + return transformAllCustomTags(content?.replace(/\n/g, ' \n')); + }, [content]); const toggleDebugDialog = () => { setDebugVisible(prevState => !prevState) } + const renderers = useMemo(() => ({ p: ({ node, ...props }) =>
    , img: ({ node, ...props }) => , @@ -325,6 +331,8 @@ export const Message = React.memo((props: MessageProps) => { return {children} } }, + toolcall: ToolCallRenderer, + thinkblock: ThinkBlockRenderer, }), []); return ( @@ -344,51 +352,17 @@ export const Message = React.memo((props: MessageProps) => { /> : icons.userChatIcon}
    -
    - - {isAi ? 'Postgres.AI' : name} - - {created_at && formattedTime && - - {formattedTime} - } -
    - {id && isPublic && <> - | - - permalink - - } - {!isLoading && isAi && id && <> - | - - } - { - aiModel && isAi && <> - | - - {aiModel} - - - } -
    -
    +
    {isLoading ? @@ -397,16 +371,21 @@ export const Message = React.memo((props: MessageProps) => { {stateMessage && stateMessage.state ? stateMessage.state : 'Thinking'}
- : + : <> + + {stateMessage && stateMessage.state &&
+ {stateMessage.state} +
} + }
diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/MessageHeader/MessageHeader.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/MessageHeader/MessageHeader.tsx new file mode 100644 index 00000000..57656e19 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/MessageHeader/MessageHeader.tsx @@ -0,0 +1,118 @@ +import React from "react"; +import cn from "classnames"; +import { permalinkLinkBuilder } from "../../../utils"; +import { makeStyles } from "@material-ui/core"; +import { colors } from "@postgres.ai/shared/styles/colors"; +import { BaseMessageProps } from "../Message"; + + +const useStyles = makeStyles( + () => ({ + messageAuthor: { + fontSize: 14, + fontWeight: 'bold', + }, + messageInfo: { + display: 'inline-block', + marginLeft: 10, + padding: 0, + fontSize: '0.75rem', + color: colors.pgaiDarkGray, + transition: '.2s ease', + background: "none", + border: "none", + textDecoration: "none", + '@media (max-width: 450px)': { + '&:nth-child(1)': { + display: 'none' + } + } + }, + messageInfoActive: { + borderBottom: '1px solid currentcolor', + cursor: 'pointer', + '&:hover': { + color: '#404040' + } + }, + messageHeader: { + height: '1.125rem', + display: 'flex', + flexWrap: 'wrap', + alignItems: 'baseline', + '@media (max-width: 450px)': { + height: 'auto', + } + }, + additionalInfo: { + '@media (max-width: 450px)': { + width: '100%', + marginTop: 4, + marginLeft: -10, + + } + }, + }), +) + +type MessageHeaderProps = Pick< + BaseMessageProps, + 'name' | 'id' | 'formattedTime' | 'isPublic' | 'isLoading' | 'aiModel' +> & { + isAi: boolean; + toggleDebugDialog: () => void; + createdAt: BaseMessageProps["created_at"]; +}; + +export const MessageHeader = (props: MessageHeaderProps) => { + const {isAi, formattedTime, id, name, createdAt, isLoading, aiModel, toggleDebugDialog, isPublic} = props; + const classes = useStyles(); + return ( +
+ + {isAi ? 'Postgres.AI' : name} + + {createdAt && formattedTime && + + {formattedTime} + + } +
+ {id && isPublic && <> + | + + permalink + + } + {!isLoading && isAi && id && <> + | + + } + { + aiModel && isAi && <> + | + + {aiModel} + + + } +
+
+ ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/ThinkingCard/ThinkingCard.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/ThinkingCard/ThinkingCard.tsx new file mode 100644 index 00000000..05b200f2 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/ThinkingCard/ThinkingCard.tsx @@ -0,0 +1,66 @@ +import React, { useState } from "react"; +import { Button } from "@postgres.ai/shared/components/Button2"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import { CardContent, Collapse } from "@mui/material"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; + +type ThinkBlockProps = { + 'data-think'?: string; + node?: { + properties?: { + 'data-think'?: string; + dataThink?: string; + }; + }; +} + +type ThinkingCardProps = { + content: string; +} + +const ThinkingCard = ({ content }: ThinkingCardProps) => { + const [expanded, setExpanded] = useState(true); + // TODO: Add "again" + // TODO: Replace with "reasoned for X seconds" + return ( + <> + + + + + + {content} + + + + + ) +} + +export const ThinkBlockRenderer = React.memo((props: ThinkBlockProps) => { + const dataThink = + props?.['data-think'] || + props?.node?.properties?.['data-think'] || + props?.node?.properties?.dataThink; + + if (!dataThink) return null; + + let rawText = ''; + try { + rawText = JSON.parse(dataThink); + } catch (err) { + console.error('Failed to parse data-think JSON:', err); + } + + return ( + + ) +}, (prevProps, nextProps) => { + return prevProps['data-think'] === nextProps['data-think']; +}) \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/Message/ToolCallRenderer/ToolCallRenderer.tsx b/ui/packages/platform/src/pages/Bot/Messages/Message/ToolCallRenderer/ToolCallRenderer.tsx new file mode 100644 index 00000000..b9192774 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Messages/Message/ToolCallRenderer/ToolCallRenderer.tsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import { SourcesShortList } from "../../Sources/SourcesShortList"; +import { SourcesFullList } from "../../Sources/SourcesFullList"; +import { Box } from "@mui/material"; + + +type MarkdownNode = { + type: string; + tagName: string; + properties?: { + ['data-json']?: string; + dataJson?: string; + }; + children?: MarkdownNode[]; +} + +type ToolCallRendererProps = { + 'data-json'?: string; + node?: MarkdownNode; +} + +export const ToolCallRenderer = (props: ToolCallRendererProps) => { + const [isSourcesVisible, setSourcesVisible] = useState(false); + + const dataJson = + props?.['data-json'] || + props?.node?.properties?.dataJson; + + if (!dataJson) { + return null; + } + + + let parsed; + try { + const preparedData = JSON.parse(dataJson); + + const cleaned = preparedData.replace(/\\n/g, '').trim(); + + parsed = JSON.parse(cleaned); + } catch (err) { + console.error("ToolCall parsing error: ", err); + return null; + } + + + const toggleSources = () => { + setSourcesVisible(prevState => !prevState) + } + + return ( + <> + + Search query: {parsed?.[0]?.arguments?.input} + Count: {parsed?.[0]?.arguments?.match_count} + Categories: {parsed?.[0]?.arguments?.categories?.join(', ')} + + + {isSourcesVisible && } + + ); +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx index abf3f54e..db9c5e4a 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx +++ b/ui/packages/platform/src/pages/Bot/Messages/Messages.tsx @@ -296,6 +296,7 @@ export const Messages = React.memo(({orgId, threadId}: {orgId: number, threadId? content={currentStreamMessage.content} aiModel={currentStreamMessage.ai_model} isCurrentStreamMessage + stateMessage={stateMessage} /> } {isWaitingForAnswer && diff --git a/ui/packages/platform/src/pages/Bot/Messages/Sources/SourceCard/SourceCard.tsx b/ui/packages/platform/src/pages/Bot/Messages/Sources/SourceCard/SourceCard.tsx new file mode 100644 index 00000000..1f6b9c53 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Messages/Sources/SourceCard/SourceCard.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import cn from 'classnames'; +import { makeStyles } from "@material-ui/core"; +import { colors } from "@postgres.ai/shared/styles/colors"; +import { ArrowDropDown, ArrowDropDownOutlined, KeyboardArrowDown, KeyboardArrowUp } from "@material-ui/icons"; + +const useStyles = makeStyles((theme) => ({ + shortContainer: { + backgroundColor: 'transparent', + border: '1px solid rgba(0, 0, 0, 0.25)', + borderRadius: '0.5rem', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + cursor: 'pointer', + width: '8rem', + height: '5rem', + padding: '0.5rem', + color: 'black', + textAlign: 'left', + fontSize: '0.938rem', + transition: '0.2s ease-in', + textDecoration: "none", + overflow: 'hidden', + '&:hover, &:focus-visible': { + border: '1px solid rgba(0, 0, 0, 0.8)', + }, + [theme.breakpoints.down(330)]: { + fontSize: '.75rem' + }, + }, + fullContainer: { + width: '100%', + height: 'auto', + border: 'none!important', + }, + showMoreContainer: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + fontSize: '1.5rem', + color: colors.pgaiDarkGray, + width: '2rem', + }, + link: { + fontSize: '0.688rem', + marginBottom: 4, + color: colors.pgaiDarkGray + }, + content: { + fontSize: '0.75rem', + display: '-webkit-box', + '-webkit-line-clamp': 3, + '-webkit-box-orient': 'vertical', + overflow: 'hidden', + textOverflow: 'ellipsis', + wordWrap: 'break-word', + overflowWrap: 'break-word', + }, + title: { + fontSize: '1rem', + display: '-webkit-box', + '-webkit-line-clamp': 2, + ' -webkit-box-orient': 'vertical', + overflow: 'hidden', + textOverflow: 'ellipsis', + fontWeight: 500 + }, + fullListCardContent: { + fontSize: '0.875rem', + marginTop: 4, + } +})); + +type SourceCardProps = { + title?: string; + content?: string; + url?: string; + variant: 'shortListCard' | 'fullListCard' | 'showMoreCard', + isVisible?: boolean; + onShowFullListClick?: () => void; +} + +export const SourceCard = (props: SourceCardProps) => { + const { title, content, url, variant, isVisible, onShowFullListClick } = props; + const classes = useStyles(); + + if (variant === 'shortListCard') { + return ( + + + {new URL(url || '').hostname} + + + {title} + + + ) + } else if (variant === 'fullListCard') { + return ( + + + {new URL(url || '').hostname} + + + {title} + + + {content} + + + ) + } else if (variant === 'showMoreCard') { + return ( + + ) + } else { + return null; + } +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/Sources/SourcesFullList.tsx b/ui/packages/platform/src/pages/Bot/Messages/Sources/SourcesFullList.tsx new file mode 100644 index 00000000..cb27a37b --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Messages/Sources/SourcesFullList.tsx @@ -0,0 +1,81 @@ +import { Box } from '@mui/material'; +import { Button } from '@postgres.ai/shared/components/Button2'; +import React, { useMemo, useState } from 'react' +import { ToolCallDataItem, ToolCallResultItem } from "../../../../types/api/entities/bot"; +import { SourceCard } from './SourceCard/SourceCard'; + + +type SourcesFullListProps = { + toolCallResult: ToolCallResultItem[] +} + +const INITIAL_COUNT = 10; + +export const SourcesFullList = (props: SourcesFullListProps) => { + const { toolCallResult } = props; + + const [visibleCount, setVisibleCount] = useState(INITIAL_COUNT); + + const sortedData = useMemo(() => { + if (!toolCallResult) return []; + + const aggregated: ToolCallDataItem[] = []; + + toolCallResult.forEach(item => { + if (item?.function_name === 'rag_search') { + aggregated.push(...item.data); + } + }); + + const uniqueItemsMap = new Map(); + + aggregated.forEach(item => { + if (item.url && !uniqueItemsMap.has(item.url)) { + uniqueItemsMap.set(item.url, item); + } + }); + + return Array.from(uniqueItemsMap.values()) + .sort((a, b) => b.similarity - a.similarity); + + }, [toolCallResult]); + + const handleShowMore = () => { + setVisibleCount((prev) => prev + INITIAL_COUNT); + }; + + const visibleItems = sortedData.slice(0, visibleCount); + + return ( + + {visibleItems.map((source) => ( + + + + ))} + + {visibleCount < sortedData.length && ( + + )} + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/Sources/SourcesShortList.tsx b/ui/packages/platform/src/pages/Bot/Messages/Sources/SourcesShortList.tsx new file mode 100644 index 00000000..86755ad5 --- /dev/null +++ b/ui/packages/platform/src/pages/Bot/Messages/Sources/SourcesShortList.tsx @@ -0,0 +1,58 @@ +import React, { useMemo } from 'react'; +import Box from "@mui/material/Box/Box"; +import { SourceCard } from "./SourceCard/SourceCard"; +import { ToolCallDataItem, ToolCallResultItem } from "../../../../types/api/entities/bot"; +import { useMediaQuery } from '@mui/material'; + +type SourcesShortListProps = { + toolCallResult: ToolCallResultItem[] + isVisible: boolean + onChangeVisibility: () => void +} + + +export const SourcesShortList = (props: SourcesShortListProps) => { + const { toolCallResult, isVisible, onChangeVisibility } = props + const isMobile = useMediaQuery('@media (max-width: 760px)') + + const sortedData = useMemo(() => { + if (!toolCallResult) return [] + + let aggregated: ToolCallDataItem[] = [] + toolCallResult.forEach(item => { + if (item?.function_name === 'rag_search') { + aggregated = aggregated.concat(item.data) + } + }) + + aggregated.sort((a, b) => b.similarity - a.similarity) + + return aggregated + }, [toolCallResult]) + + const visibleCount = isMobile ? 2 : 4 + const visibleItems = sortedData.slice(0, visibleCount) + + return ( + + {visibleItems.map((source, index) => ( + + + + ))} + + {sortedData.length > visibleCount && ( + + )} + + ) +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/Messages/utils.ts b/ui/packages/platform/src/pages/Bot/Messages/utils.ts index c10dc2e1..017fedfd 100644 --- a/ui/packages/platform/src/pages/Bot/Messages/utils.ts +++ b/ui/packages/platform/src/pages/Bot/Messages/utils.ts @@ -1,10 +1,3 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - import {BotMessage} from "../../../types/api/entities/bot"; export const getMaxScrollTop = (element: HTMLElement) => @@ -23,3 +16,106 @@ export const getUserMessagesCount = (messages: BotMessage[]) => { return !messages[idx].is_ai ? count + 1 : count }, 0) } + +const THINK_REGEX = /([\s\S]*?)<\/think>/g; +const TOOLCALL_REGEX = /([\s\S]*?)<\/toolcall>/g; + +export function unescapeHtml(escaped: string): string { + return escaped + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'"); +} + +const THINK_OPEN = ''; +const THINK_CLOSE = ''; + +/* WIP: Rendering refactoring must be done in the future */ +function transformThinkingBlocksPartial(text: string): string { + let result = ''; + let currentIndex = 0; + + while (true) { + const openIdx = text.indexOf(THINK_OPEN, currentIndex); + if (openIdx === -1) { + result += text.slice(currentIndex); + break; + } + + result += text.slice(currentIndex, openIdx); + + const afterOpen = openIdx + THINK_OPEN.length; + const closeIdx = text.indexOf(THINK_CLOSE, afterOpen); + if (closeIdx === -1) { + const partialContent = text.slice(afterOpen); + result += makeThinkblockHTML(partialContent, false); + break; + } else { + const finalContent = text.slice(afterOpen, closeIdx); + result += makeThinkblockHTML(finalContent, true); + currentIndex = closeIdx + THINK_CLOSE.length; + } + } + + return result; +} + +function transformThinkingBlocksFinal(text: string): string { + return text.replace(THINK_REGEX, (_, innerContent) => { + return makeThinkblockHTML(innerContent, true); + }); +} + +function makeThinkblockHTML(content: string, isFinal: boolean): string { + const status = isFinal ? 'final' : 'partial'; + let json = JSON.stringify(content); + json = json + .replace(/'/g, '\\u0027') + .replace(//g, '\\u003e') + .replace(/&/g, '\\u0026'); + + return ` + + + +`; +} + +function makeToolCallHTML(content: string): string { + let json = JSON.stringify(content); + + json = json + .replace(/'/g, '\\u0027') + .replace(//g, '\\u003e') + .replace(/&/g, '\\u0026'); + + return ` + + + +`; +} + +function transformToolCallBlocksFinal(text: string): string { + return text.replace(TOOLCALL_REGEX, (_, innerContent: string) => { + return makeToolCallHTML(innerContent); + }); +} + +export function transformAllCustomTags(text: string): string { + let result = text; + + if (text.includes("") && text.includes("")) { + result = transformThinkingBlocksFinal(text); + } + + if (result.includes("") && result.includes("")) { + result = transformToolCallBlocksFinal(result); + } + + return result; +} \ No newline at end of file diff --git a/ui/packages/platform/src/pages/Bot/hooks.tsx b/ui/packages/platform/src/pages/Bot/hooks.tsx index fc08ecb9..9c103847 100644 --- a/ui/packages/platform/src/pages/Bot/hooks.tsx +++ b/ui/packages/platform/src/pages/Bot/hooks.tsx @@ -5,7 +5,15 @@ *-------------------------------------------------------------------------- */ -import React, { createContext, Dispatch, SetStateAction, useCallback, useContext, useEffect, useState } from "react"; +import React, { + createContext, + Dispatch, + SetStateAction, + useCallback, + useContext, + useEffect, + useState +} from "react"; import useWebSocket, {ReadyState} from "react-use-websocket"; import { useLocation } from "react-router-dom"; import { @@ -14,7 +22,8 @@ import { AiModel, StateMessage, StreamMessage, - ErrorMessage, MessageStatus + ErrorMessage, + MessageStatus } from "../../types/api/entities/bot"; import {getChatsWithWholeThreads} from "../../api/bot/getChatsWithWholeThreads"; import {getChats} from "api/bot/getChats"; @@ -108,9 +117,9 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => const [error, setError] = useState(null); const [wsLoading, setWsLoading] = useState(false); const [chatVisibility, setChatVisibility] = useState(Visibility.PUBLIC); - const [stateMessage, setStateMessage] = useState(null) - const [currentStreamMessage, setCurrentStreamMessage] = useState(null) - const [isStreamingInProcess, setStreamingInProcess] = useState(false) + const [stateMessage, setStateMessage] = useState(null); + const [currentStreamMessage, setCurrentStreamMessage] = useState(null); + const [isStreamingInProcess, setStreamingInProcess] = useState(false); const [isChangeVisibilityLoading, setIsChangeVisibilityLoading] = useState(false); @@ -131,8 +140,9 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => const isStateMessage = messageData.type === 'state'; const isStreamMessage = messageData.type === 'stream'; const isErrorMessage = messageData.type === 'error'; + const isToolCallResultMessage = messageData.type === 'tool_call_result'; - if (isThreadMatching || isParentMatching || isDebugMessage || isStateMessage || isStreamMessage || isErrorMessage) { + if (isThreadMatching || isParentMatching || isDebugMessage || isStateMessage || isStreamMessage || isErrorMessage || isToolCallResultMessage) { switch (messageData.type) { case 'debug': handleDebugMessage(messageData) @@ -149,6 +159,9 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => case 'error': handleErrorMessage(messageData) break; + case 'tool_call_result': + handleToolCallResultMessage(messageData) + break; } } else if (threadId !== messageData.thread_id) { const threadInList = chatsList?.find((item) => item.thread_id === messageData.thread_id) @@ -217,6 +230,21 @@ export const useAiBotProviderValue = (args: UseAiBotArgs): UseAiBotReturnType => } } + const handleToolCallResultMessage = (message: BotMessage) => { + if (messages && messages.length > 0) { + let currentMessages = [...messages]; + const lastMessage = currentMessages[currentMessages.length - 1]; + if (lastMessage && !lastMessage.id && message.parent_id) { + lastMessage.id = message.parent_id; + lastMessage.created_at = message.created_at; + lastMessage.is_public = message.is_public; + } + + currentMessages.push(message); + setMessages(currentMessages); + } + } + const handleErrorMessage = (message: ErrorMessage) => { if (message && message.message) { let error = { diff --git a/ui/packages/platform/src/types/api/entities/bot.ts b/ui/packages/platform/src/types/api/entities/bot.ts index c30dd25d..fd5567e2 100644 --- a/ui/packages/platform/src/types/api/entities/bot.ts +++ b/ui/packages/platform/src/types/api/entities/bot.ts @@ -21,7 +21,7 @@ export type BotMessage = { user_id: number org_id: string thread_id: string - type: 'message' | undefined + type: 'message' | 'tool_call_result' | undefined ai_model: string status?: MessageStatus } @@ -57,4 +57,22 @@ export type ErrorMessage = { thread_id: string } -export type MessageStatus = 'read' | 'new' | null \ No newline at end of file +export type MessageStatus = 'read' | 'new' | null + +export type ToolCallDataItem = { + similarity: number + url: string + category: string + title: string + content: string +} + +export type ToolCallResultItem = { + function_name: string + arguments: { + input: string, + match_count: number + categories: string[] + } + data: ToolCallDataItem[] +} \ No newline at end of file From 80f8fe825c31732df762a07b7c9aee5da0908fad Mon Sep 17 00:00:00 2001 From: Bogdan Tsechoev Date: Wed, 9 Apr 2025 14:21:36 +0000 Subject: [PATCH 110/111] Clean up old code --- CONTRIBUTING.md | 1 - ui/.dockerignore | 1 - ui/.gitlab-ci.yml | 2 - ui/README.md | 21 +- ui/package.json | 1 - ui/packages/ce/.dockerignore | 3 +- ui/packages/platform/.dockerignore | 10 - ui/packages/platform/.env_example_dev | 3 - ui/packages/platform/.eslintrc | 7 - ui/packages/platform/.gitignore | 1 - ui/packages/platform/.gitlab-ci.yml | 131 - ui/packages/platform/.npmrc | 2 - ui/packages/platform/.stylelintrc | 8 - ui/packages/platform/COPYRIGHT | 6 - ui/packages/platform/Dockerfile | 67 - ui/packages/platform/README.md | 17 - ui/packages/platform/ci_docker_build_push.sh | 50 - ui/packages/platform/craco.config.js | 3 - ui/packages/platform/deploy/configs/dev.sh | 26 - .../deploy/configs/dev1.imgdata.ru.sh | 21 - ui/packages/platform/deploy/configs/local.sh | 25 - .../platform/deploy/configs/production.sh | 27 - .../platform/deploy/configs/staging.sh | 25 - .../platform/deploy/platform-console.yaml | 53 - ui/packages/platform/do.sh | 59 - ui/packages/platform/nginx.conf | 49 - ui/packages/platform/package.json | 123 - ui/packages/platform/public/auth-gate.html | 31 - ui/packages/platform/public/favicon.ico | Bin 8490 -> 0 bytes .../platform/public/images/ansible.svg | 2 - ui/packages/platform/public/images/avatar.jpg | Bin 62337 -> 0 bytes .../platform/public/images/bot_avatar.png | Bin 187908 -> 0 bytes ui/packages/platform/public/images/dblab.svg | 10 - ui/packages/platform/public/images/docker.svg | 4 - ui/packages/platform/public/images/globe.svg | 2 - .../platform/public/images/infosrc.png | Bin 1109 -> 0 bytes .../public/images/oauth-github-logo.png | Bin 1151 -> 0 bytes .../public/images/oauth-gitlab-logo.png | Bin 8464 -> 0 bytes .../public/images/oauth-google-logo.png | Bin 1625 -> 0 bytes .../public/images/oauth-linkedin-logo.png | Bin 6132 -> 0 bytes .../public/images/paymentMethods/amex.png | Bin 6726 -> 0 bytes .../public/images/paymentMethods/diners.png | Bin 6921 -> 0 bytes .../public/images/paymentMethods/discover.png | Bin 5742 -> 0 bytes .../public/images/paymentMethods/maestro.png | Bin 6675 -> 0 bytes .../images/paymentMethods/mastercard.png | Bin 6281 -> 0 bytes .../public/images/paymentMethods/unionpay.png | Bin 32719 -> 0 bytes .../public/images/paymentMethods/visa.png | Bin 5167 -> 0 bytes .../public/images/service-providers/aws.png | Bin 4071 -> 0 bytes .../images/service-providers/digitalocean.png | Bin 6712 -> 0 bytes .../public/images/service-providers/gcp.png | Bin 4124 -> 0 bytes .../images/service-providers/hetzner.png | Bin 6938 -> 0 bytes ui/packages/platform/public/images/simple.svg | 7 - .../platform/public/images/warning.png | Bin 1906 -> 0 bytes ui/packages/platform/public/index.html | 49 - ui/packages/platform/public/manifest.json | 15 - ui/packages/platform/src/App.jsx | 50 - ui/packages/platform/src/App.test.js | 17 - ui/packages/platform/src/actions/actions.js | 1760 ---------- ui/packages/platform/src/api/api.js | 1127 ------ .../src/api/billing/getPaymentMethods.ts | 18 - .../src/api/billing/getSubscription.ts | 18 - .../src/api/billing/startBillingSession.ts | 19 - .../platform/src/api/bot/convertThread.ts | 24 - .../platform/src/api/bot/getAiModels.ts | 30 - ui/packages/platform/src/api/bot/getChats.ts | 29 - .../src/api/bot/getChatsWithWholeThreads.ts | 30 - .../platform/src/api/bot/getDebugMessages.ts | 39 - .../src/api/bot/updateChatVisibility.ts | 36 - .../platform/src/api/clones/createClone.ts | 39 - .../platform/src/api/clones/destroyClone.ts | 26 - .../platform/src/api/clones/getClone.ts | 29 - .../platform/src/api/clones/resetClone.ts | 30 - .../platform/src/api/clones/updateClone.ts | 22 - .../platform/src/api/cloud/getCloudImages.ts | 38 - .../src/api/cloud/getCloudInstances.ts | 41 - .../src/api/cloud/getCloudProviders.ts | 22 - .../platform/src/api/cloud/getCloudRegions.ts | 25 - .../platform/src/api/cloud/getCloudVolumes.ts | 30 - .../platform/src/api/cloud/getOrgKeys.ts | 10 - .../platform/src/api/configs/getConfig.ts | 11 - .../platform/src/api/configs/getFullConfig.ts | 14 - .../platform/src/api/configs/getSeImages.ts | 21 - .../platform/src/api/configs/getTaskState.ts | 16 - .../src/api/configs/initStreamLogs.ts | 11 - .../platform/src/api/configs/launchDeploy.ts | 147 - .../src/api/configs/regenerateCode.ts | 22 - .../platform/src/api/configs/testDbSource.ts | 21 - .../platform/src/api/configs/updateConfig.ts | 65 - .../platform/src/api/engine/getEngine.ts | 16 - .../platform/src/api/engine/getWSToken.ts | 14 - ui/packages/platform/src/api/engine/initWS.ts | 10 - .../platform/src/api/explain/depesz.js | 36 - ui/packages/platform/src/api/explain/pev2.js | 37 - ui/packages/platform/src/api/getMeta.ts | 12 - .../platform/src/api/instances/getInstance.ts | 30 - .../platform/src/api/instances/getWSToken.ts | 59 - .../src/api/instances/refreshInstance.ts | 24 - .../src/api/snapshots/getSnapshots.ts | 30 - .../platform/src/assets/explainSamples.ts | 600 ---- ui/packages/platform/src/assets/messages.ts | 11 - ui/packages/platform/src/assets/plans.ts | 26 - .../platform/src/assets/visualizeTypes.ts | 12 - .../components/AccessTokens/AccessTokens.tsx | 545 --- .../AccessTokens/AccessTokensWrapper.tsx | 70 - .../FilteredTableMessage.tsx | 39 - .../AddDbLabInstanceFormWrapper.tsx | 62 - .../AddDblabInstanceForm.tsx | 646 ---- .../AddMemberForm/AddMemberForm.tsx | 238 -- .../AddMemberForm/AddMemberFormWrapper.tsx | 44 - .../src/components/AppUpdateBanner/index.tsx | 26 - .../AppUpdateBanner/styles.module.scss | 14 - .../platform/src/components/Audit/Audit.tsx | 411 --- .../src/components/Audit/AuditWrapper.tsx | 98 - .../AuditSettingsForm/AuditSettingsForm.tsx | 426 --- .../AuditSettingsFormWrapper.tsx | 32 - .../src/components/Billing/Billing.tsx | 180 - .../src/components/Billing/BillingWrapper.tsx | 77 - .../BotSettingsForm/BotSettingsForm.tsx | 303 -- .../BotSettingsFormWrapper.tsx | 32 - .../CheckupAgentForm/CheckupAgentForm.tsx | 1031 ------ .../CheckupAgentFormWrapper.tsx | 149 - .../ConsoleBreadcrumbs/ConsoleBreadcrumbs.tsx | 156 - .../ConsoleBreadcrumbsWrapper.tsx | 59 - .../ConsoleButton/ConsoleButton.tsx | 34 - .../ConsoleButton/ConsoleButtonWrapper.tsx | 28 - .../src/components/ConsolePageTitle.tsx | 162 - .../ContentLayout/DemoOrgNotice/index.tsx | 76 - .../DeprecatedApiBanner/index.tsx | 47 - .../components/ContentLayout/Footer/index.tsx | 120 - .../src/components/ContentLayout/index.tsx | 65 - .../ContentLayout/styles.module.scss | 46 - .../CreateClusterCards/CreateClusterCards.tsx | 117 - .../CreateDbLabCards/CreateDbLabCards.tsx | 182 - .../DBLabSettingsForm/DBLabSettingsForm.tsx | 413 --- .../DBLabSettingsFormWrapper.tsx | 30 - .../src/components/Dashboard/Dashboard.tsx | 618 ---- .../components/Dashboard/DashboardWrapper.tsx | 136 - .../DbLabFormSteps/AnsibleInstance.tsx | 315 -- .../DbLabFormSteps/DockerInstance.tsx | 181 - .../DbLabFormSteps/InstanceFormCreation.tsx | 133 - .../DbLabFormSteps/SimpleInstance.tsx | 587 ---- .../DbLabFormSteps/streamLogs.ts | 74 - .../DbLabInstanceForm/DbLabInstanceForm.tsx | 584 ---- .../DbLabInstanceFormSidebar.tsx | 259 -- .../DbLabInstanceFormSlider.tsx | 72 - .../DbLabInstanceFormWrapper.tsx | 311 -- .../DbLabInstanceForm/reducer/index.tsx | 192 - .../DbLabInstanceForm/utils/index.tsx | 278 -- .../DbLabFormSteps/AnsibleInstance.tsx | 148 - .../DbLabFormSteps/DockerInstance.tsx | 118 - .../DbLabFormSteps/SetupStep.tsx | 22 - .../DbLabInstanceInstallForm.tsx | 240 -- .../DbLabInstanceInstallFormSidebar.tsx | 109 - .../DbLabInstanceInstallFormWrapper.tsx | 29 - .../reducer/index.tsx | 60 - .../DbLabInstanceInstallForm/utils/index.ts | 40 - .../DbLabInstances/DbLabInstances.tsx | 625 ---- .../DbLabInstances/DbLabInstancesWrapper.tsx | 103 - .../components/DbLabSession/DbLabSession.tsx | 1018 ------ .../DbLabSession/DbLabSessionWrapper.tsx | 269 -- .../DbLabSessions/DbLabSessions.tsx | 404 --- .../DbLabSessions/DbLabSessionsWrapper.tsx | 39 - .../components/DbLabStatus/DbLabStatus.tsx | 216 -- .../DbLabStatus/DbLabStatusWrapper.tsx | 147 - .../components/DisplayToken/DisplayToken.tsx | 127 - .../DisplayToken/DisplayTokenWrapper.tsx | 33 - .../platform/src/components/Error/Error.tsx | 46 - .../src/components/Error/ErrorWrapper.tsx | 32 - .../ExplainVisualization.tsx | 295 -- .../ExplainVisualizationWrapper.tsx | 59 - .../platform/src/components/FlameGraph.tsx | 318 -- .../platform/src/components/Head/index.tsx | 30 - .../src/components/IndexPage/IndexPage.tsx | 1633 --------- .../components/IndexPage/IndexPageWrapper.tsx | 385 -- .../platform/src/components/JoeConfig.tsx | 35 - .../src/components/JoeHistory/JoeHistory.tsx | 1069 ------ .../JoeHistory/JoeHistoryWrapper.tsx | 165 - .../JoeInstanceForm/JoeInstanceForm.tsx | 502 --- .../JoeInstanceFormWrapper.tsx | 63 - .../components/JoeInstances/JoeInstances.tsx | 482 --- .../JoeInstances/JoeInstancesWrapper.tsx | 64 - .../src/components/KBStats/KBStats.tsx | 72 - .../platform/src/components/KBStats/hooks.ts | 41 - .../components/LoginDialog/LoginDialog.tsx | 200 -- .../LoginDialog/LoginDialogWrapper.tsx | 17 - .../components/Notification/Notification.tsx | 139 - .../Notification/NotificationWrapper.tsx | 39 - .../src/components/OrgForm/OrgForm.tsx | 802 ----- .../src/components/OrgForm/OrgFormWrapper.tsx | 129 - .../src/components/OrgMembers/OrgMembers.tsx | 439 --- .../OrgMembers/OrgMembersWrapper.tsx | 80 - .../PostgresClusterForm/PostgresCluster.tsx | 809 ----- .../PostgresClusterSteps/index.tsx | 252 -- .../PostgresClusterWrapper.tsx | 26 - .../PostgresClusterForm/reducer/index.tsx | 296 -- .../PostgresClusterForm/utils/index.tsx | 49 - .../PostgresClusterInstallForm.tsx | 360 -- .../PostgresClusterInstallFormSidebar.tsx | 111 - .../PostgresClusterInstallWrapper.tsx | 28 - .../PostgresClusterSteps/AnsibleInstance.tsx | 133 - .../PostgresClusterSteps/DockerInstance.tsx | 113 - .../reducer/index.tsx | 191 - .../utils/index.tsx | 120 - .../PostgresClusters/PostgresClusters.tsx | 606 ---- .../PostgresClustersWrapper.tsx | 103 - .../components/ProductCard/ProductCard.tsx | 59 - .../ProductCard/ProductCardWrapper.tsx | 123 - .../platform/src/components/Report/Report.tsx | 345 -- .../src/components/Report/ReportWrapper.tsx | 61 - .../src/components/ReportFile/ReportFile.tsx | 428 --- .../ReportFile/ReportFileWrapper.tsx | 84 - .../src/components/Reports/Reports.tsx | 586 ---- .../src/components/Reports/ReportsWrapper.tsx | 57 - .../SIEMIntegrationForm.tsx | 181 - .../ShareUrlDialog/ShareUrlDialog.tsx | 330 -- .../ShareUrlDialog/ShareUrlDialogWrapper.tsx | 49 - .../src/components/SharedUrl/SharedUrl.tsx | 178 - .../components/SharedUrl/SharedUrlWrapper.tsx | 81 - .../platform/src/components/SideNav/index.tsx | 62 - .../src/components/StripeForm/index.tsx | 531 --- .../components/StripeForm/stripeStyles.tsx | 80 - .../src/components/Warning/Warning.tsx | 43 - .../src/components/Warning/WarningWrapper.tsx | 64 - .../platform/src/components/types/index.ts | 204 -- ui/packages/platform/src/config/emoji.ts | 909 ----- ui/packages/platform/src/config/env.ts | 19 - .../platform/src/config/routes/clones.ts | 67 - .../platform/src/config/routes/index.ts | 66 - .../platform/src/config/routes/instances.ts | 41 - .../platform/src/helpers/localStorage.ts | 12 - ui/packages/platform/src/helpers/request.ts | 28 - .../src/helpers/simpleInstallRequest.ts | 34 - .../platform/src/hooks/useCloudProvider.ts | 183 - ui/packages/platform/src/hooks/usePrev.ts | 18 - ui/packages/platform/src/index.scss | 27 - ui/packages/platform/src/index.tsx | 41 - ui/packages/platform/src/meta.json | 1 - .../platform/src/pages/Bot/BotWrapper.tsx | 49 - .../src/pages/Bot/ChatsList/ChatsList.tsx | 220 -- .../src/pages/Bot/Command/Command.tsx | 215 -- .../src/pages/Bot/Command/useBuffer.ts | 53 - .../src/pages/Bot/Command/useCaret.ts | 22 - .../platform/src/pages/Bot/Command/utils.ts | 67 - .../pages/Bot/DebugConsole/DebugConsole.tsx | 114 - .../src/pages/Bot/DebugDialog/DebugDialog.tsx | 111 - .../src/pages/Bot/DebugLogs/DebugLogs.tsx | 31 - .../pages/Bot/HeaderButtons/HeaderButtons.tsx | 100 - .../ArrowGrowthIcon/ArrowGrowthIcon.tsx | 15 - .../CommonTypeIcon/CommonTypeIcon.tsx | 14 - .../pages/Bot/HintCards/HintCard/HintCard.tsx | 78 - .../src/pages/Bot/HintCards/HintCards.tsx | 40 - .../Bot/HintCards/TableIcon/TableIcon.tsx | 19 - .../Bot/HintCards/WrenchIcon/WrenchIcon.tsx | 14 - .../Messages/ErrorMessage/ErrorMessage.tsx | 27 - .../Messages/Message/CodeBlock/CodeBlock.tsx | 160 - .../Message/MermaidDiagram/MermaidDiagram.tsx | 176 - .../MermaidDiagram/MermaidDiagramControls.tsx | 144 - .../pages/Bot/Messages/Message/Message.tsx | 394 --- .../Message/MessageHeader/MessageHeader.tsx | 118 - .../Message/ThinkingCard/ThinkingCard.tsx | 66 - .../ToolCallRenderer/ToolCallRenderer.tsx | 74 - .../src/pages/Bot/Messages/Messages.tsx | 312 -- .../Sources/SourceCard/SourceCard.tsx | 126 - .../Bot/Messages/Sources/SourcesFullList.tsx | 81 - .../Bot/Messages/Sources/SourcesShortList.tsx | 58 - .../platform/src/pages/Bot/Messages/utils.ts | 121 - .../pages/Bot/ModelSelector/ModelSelector.tsx | 81 - .../Bot/SettingsDialog/SettingsDialog.tsx | 387 -- .../pages/Bot/SettingsPanel/SettingsPanel.tsx | 113 - ui/packages/platform/src/pages/Bot/hints.ts | 30 - ui/packages/platform/src/pages/Bot/hooks.tsx | 705 ---- ui/packages/platform/src/pages/Bot/index.tsx | 286 -- ui/packages/platform/src/pages/Bot/utils.ts | 117 - .../platform/src/pages/Clone/index.tsx | 72 - .../pages/Consulting/ConsultingWrapper.tsx | 25 - .../TransactionsTable/TransactionsTable.tsx | 42 - .../platform/src/pages/Consulting/index.tsx | 196 -- .../platform/src/pages/Consulting/utils.ts | 30 - .../platform/src/pages/CreateClone/index.tsx | 67 - .../platform/src/pages/Instance/index.tsx | 115 - .../src/pages/JoeInstance/Command/index.tsx | 164 - .../pages/JoeInstance/Command/useBuffer.ts | 53 - .../src/pages/JoeInstance/Command/useCaret.ts | 22 - .../src/pages/JoeInstance/Command/utils.ts | 57 - .../pages/JoeInstance/JoeInstanceWrapper.jsx | 268 -- .../JoeInstance/Messages/Banner/index.tsx | 37 - .../Messages/Banner/styles.module.scss | 23 - .../src/pages/JoeInstance/Messages/index.jsx | 284 -- .../JoeInstance/Messages/styles.module.scss | 23 - .../src/pages/JoeInstance/Messages/utils.ts | 42 - .../platform/src/pages/JoeInstance/index.jsx | 453 --- .../src/pages/JoeInstance/styles.scss | 101 - .../platform/src/pages/JoeInstance/utils.ts | 43 - .../JoeSessionCommandWrapper.jsx | 51 - .../JoeSessionCommand/TabPanel/index.tsx | 42 - .../src/pages/JoeSessionCommand/index.js | 507 --- .../src/pages/Profile/ProfileWrapper.tsx | 63 - .../platform/src/pages/Profile/index.jsx | 254 -- .../src/pages/SignIn/SignInWrapper.jsx | 92 - .../platform/src/pages/SignIn/index.jsx | 69 - ui/packages/platform/src/react-app-env.d.ts | 18 - .../src/react-syntax-highlighter.d.ts | 4 - .../platform/src/registerServiceWorker.js | 126 - ui/packages/platform/src/stores/app.ts | 32 - ui/packages/platform/src/stores/banners.ts | 19 - ui/packages/platform/src/stores/consulting.ts | 101 - .../src/stores/preformatJoeMessage.ts | 43 - ui/packages/platform/src/stores/store.js | 3110 ----------------- .../platform/src/types/api/entities/bot.ts | 78 - .../platform/src/types/api/entities/meta.ts | 7 - ui/packages/platform/src/utils/aliases.ts | 122 - ui/packages/platform/src/utils/cfggen.ts | 332 -- ui/packages/platform/src/utils/dblabutils.ts | 50 - ui/packages/platform/src/utils/format.ts | 291 -- ui/packages/platform/src/utils/permissions.ts | 106 - ui/packages/platform/src/utils/settings.ts | 42 - ui/packages/platform/src/utils/time.ts | 42 - ui/packages/platform/src/utils/urls.ts | 228 -- ui/packages/platform/src/utils/utils.ts | 104 - ui/packages/platform/src/utils/webSockets.ts | 13 - ui/packages/platform/tsconfig.json | 26 - ui/pnpm-lock.yaml | 3086 ++-------------- 322 files changed, 205 insertions(+), 50094 deletions(-) delete mode 100644 ui/packages/platform/.dockerignore delete mode 100644 ui/packages/platform/.env_example_dev delete mode 100644 ui/packages/platform/.eslintrc delete mode 100644 ui/packages/platform/.gitignore delete mode 100644 ui/packages/platform/.gitlab-ci.yml delete mode 100644 ui/packages/platform/.npmrc delete mode 100644 ui/packages/platform/.stylelintrc delete mode 100644 ui/packages/platform/COPYRIGHT delete mode 100644 ui/packages/platform/Dockerfile delete mode 100644 ui/packages/platform/README.md delete mode 100644 ui/packages/platform/ci_docker_build_push.sh delete mode 100644 ui/packages/platform/craco.config.js delete mode 100644 ui/packages/platform/deploy/configs/dev.sh delete mode 100644 ui/packages/platform/deploy/configs/dev1.imgdata.ru.sh delete mode 100644 ui/packages/platform/deploy/configs/local.sh delete mode 100644 ui/packages/platform/deploy/configs/production.sh delete mode 100644 ui/packages/platform/deploy/configs/staging.sh delete mode 100644 ui/packages/platform/deploy/platform-console.yaml delete mode 100644 ui/packages/platform/do.sh delete mode 100644 ui/packages/platform/nginx.conf delete mode 100644 ui/packages/platform/package.json delete mode 100644 ui/packages/platform/public/auth-gate.html delete mode 100644 ui/packages/platform/public/favicon.ico delete mode 100644 ui/packages/platform/public/images/ansible.svg delete mode 100644 ui/packages/platform/public/images/avatar.jpg delete mode 100644 ui/packages/platform/public/images/bot_avatar.png delete mode 100644 ui/packages/platform/public/images/dblab.svg delete mode 100644 ui/packages/platform/public/images/docker.svg delete mode 100644 ui/packages/platform/public/images/globe.svg delete mode 100644 ui/packages/platform/public/images/infosrc.png delete mode 100644 ui/packages/platform/public/images/oauth-github-logo.png delete mode 100644 ui/packages/platform/public/images/oauth-gitlab-logo.png delete mode 100644 ui/packages/platform/public/images/oauth-google-logo.png delete mode 100644 ui/packages/platform/public/images/oauth-linkedin-logo.png delete mode 100644 ui/packages/platform/public/images/paymentMethods/amex.png delete mode 100644 ui/packages/platform/public/images/paymentMethods/diners.png delete mode 100644 ui/packages/platform/public/images/paymentMethods/discover.png delete mode 100644 ui/packages/platform/public/images/paymentMethods/maestro.png delete mode 100644 ui/packages/platform/public/images/paymentMethods/mastercard.png delete mode 100644 ui/packages/platform/public/images/paymentMethods/unionpay.png delete mode 100644 ui/packages/platform/public/images/paymentMethods/visa.png delete mode 100644 ui/packages/platform/public/images/service-providers/aws.png delete mode 100644 ui/packages/platform/public/images/service-providers/digitalocean.png delete mode 100644 ui/packages/platform/public/images/service-providers/gcp.png delete mode 100644 ui/packages/platform/public/images/service-providers/hetzner.png delete mode 100644 ui/packages/platform/public/images/simple.svg delete mode 100644 ui/packages/platform/public/images/warning.png delete mode 100644 ui/packages/platform/public/index.html delete mode 100644 ui/packages/platform/public/manifest.json delete mode 100644 ui/packages/platform/src/App.jsx delete mode 100644 ui/packages/platform/src/App.test.js delete mode 100644 ui/packages/platform/src/actions/actions.js delete mode 100644 ui/packages/platform/src/api/api.js delete mode 100644 ui/packages/platform/src/api/billing/getPaymentMethods.ts delete mode 100644 ui/packages/platform/src/api/billing/getSubscription.ts delete mode 100644 ui/packages/platform/src/api/billing/startBillingSession.ts delete mode 100644 ui/packages/platform/src/api/bot/convertThread.ts delete mode 100644 ui/packages/platform/src/api/bot/getAiModels.ts delete mode 100644 ui/packages/platform/src/api/bot/getChats.ts delete mode 100644 ui/packages/platform/src/api/bot/getChatsWithWholeThreads.ts delete mode 100644 ui/packages/platform/src/api/bot/getDebugMessages.ts delete mode 100644 ui/packages/platform/src/api/bot/updateChatVisibility.ts delete mode 100644 ui/packages/platform/src/api/clones/createClone.ts delete mode 100644 ui/packages/platform/src/api/clones/destroyClone.ts delete mode 100644 ui/packages/platform/src/api/clones/getClone.ts delete mode 100644 ui/packages/platform/src/api/clones/resetClone.ts delete mode 100644 ui/packages/platform/src/api/clones/updateClone.ts delete mode 100644 ui/packages/platform/src/api/cloud/getCloudImages.ts delete mode 100644 ui/packages/platform/src/api/cloud/getCloudInstances.ts delete mode 100644 ui/packages/platform/src/api/cloud/getCloudProviders.ts delete mode 100644 ui/packages/platform/src/api/cloud/getCloudRegions.ts delete mode 100644 ui/packages/platform/src/api/cloud/getCloudVolumes.ts delete mode 100644 ui/packages/platform/src/api/cloud/getOrgKeys.ts delete mode 100644 ui/packages/platform/src/api/configs/getConfig.ts delete mode 100644 ui/packages/platform/src/api/configs/getFullConfig.ts delete mode 100644 ui/packages/platform/src/api/configs/getSeImages.ts delete mode 100644 ui/packages/platform/src/api/configs/getTaskState.ts delete mode 100644 ui/packages/platform/src/api/configs/initStreamLogs.ts delete mode 100644 ui/packages/platform/src/api/configs/launchDeploy.ts delete mode 100644 ui/packages/platform/src/api/configs/regenerateCode.ts delete mode 100644 ui/packages/platform/src/api/configs/testDbSource.ts delete mode 100644 ui/packages/platform/src/api/configs/updateConfig.ts delete mode 100644 ui/packages/platform/src/api/engine/getEngine.ts delete mode 100644 ui/packages/platform/src/api/engine/getWSToken.ts delete mode 100644 ui/packages/platform/src/api/engine/initWS.ts delete mode 100644 ui/packages/platform/src/api/explain/depesz.js delete mode 100644 ui/packages/platform/src/api/explain/pev2.js delete mode 100644 ui/packages/platform/src/api/getMeta.ts delete mode 100644 ui/packages/platform/src/api/instances/getInstance.ts delete mode 100644 ui/packages/platform/src/api/instances/getWSToken.ts delete mode 100644 ui/packages/platform/src/api/instances/refreshInstance.ts delete mode 100644 ui/packages/platform/src/api/snapshots/getSnapshots.ts delete mode 100644 ui/packages/platform/src/assets/explainSamples.ts delete mode 100644 ui/packages/platform/src/assets/messages.ts delete mode 100644 ui/packages/platform/src/assets/plans.ts delete mode 100644 ui/packages/platform/src/assets/visualizeTypes.ts delete mode 100644 ui/packages/platform/src/components/AccessTokens/AccessTokens.tsx delete mode 100644 ui/packages/platform/src/components/AccessTokens/AccessTokensWrapper.tsx delete mode 100644 ui/packages/platform/src/components/AccessTokens/FilteredTableMessage/FilteredTableMessage.tsx delete mode 100644 ui/packages/platform/src/components/AddDbLabInstanceFormWrapper/AddDbLabInstanceFormWrapper.tsx delete mode 100644 ui/packages/platform/src/components/AddDbLabInstanceFormWrapper/AddDblabInstanceForm.tsx delete mode 100644 ui/packages/platform/src/components/AddMemberForm/AddMemberForm.tsx delete mode 100644 ui/packages/platform/src/components/AddMemberForm/AddMemberFormWrapper.tsx delete mode 100644 ui/packages/platform/src/components/AppUpdateBanner/index.tsx delete mode 100644 ui/packages/platform/src/components/AppUpdateBanner/styles.module.scss delete mode 100644 ui/packages/platform/src/components/Audit/Audit.tsx delete mode 100644 ui/packages/platform/src/components/Audit/AuditWrapper.tsx delete mode 100644 ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx delete mode 100644 ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsFormWrapper.tsx delete mode 100644 ui/packages/platform/src/components/Billing/Billing.tsx delete mode 100644 ui/packages/platform/src/components/Billing/BillingWrapper.tsx delete mode 100644 ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx delete mode 100644 ui/packages/platform/src/components/BotSettingsForm/BotSettingsFormWrapper.tsx delete mode 100644 ui/packages/platform/src/components/CheckupAgentForm/CheckupAgentForm.tsx delete mode 100644 ui/packages/platform/src/components/CheckupAgentForm/CheckupAgentFormWrapper.tsx delete mode 100644 ui/packages/platform/src/components/ConsoleBreadcrumbs/ConsoleBreadcrumbs.tsx delete mode 100644 ui/packages/platform/src/components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper.tsx delete mode 100644 ui/packages/platform/src/components/ConsoleButton/ConsoleButton.tsx delete mode 100644 ui/packages/platform/src/components/ConsoleButton/ConsoleButtonWrapper.tsx delete mode 100644 ui/packages/platform/src/components/ConsolePageTitle.tsx delete mode 100644 ui/packages/platform/src/components/ContentLayout/DemoOrgNotice/index.tsx delete mode 100644 ui/packages/platform/src/components/ContentLayout/DeprecatedApiBanner/index.tsx delete mode 100644 ui/packages/platform/src/components/ContentLayout/Footer/index.tsx delete mode 100644 ui/packages/platform/src/components/ContentLayout/index.tsx delete mode 100644 ui/packages/platform/src/components/ContentLayout/styles.module.scss delete mode 100644 ui/packages/platform/src/components/CreateClusterCards/CreateClusterCards.tsx delete mode 100644 ui/packages/platform/src/components/CreateDbLabCards/CreateDbLabCards.tsx delete mode 100644 ui/packages/platform/src/components/DBLabSettingsForm/DBLabSettingsForm.tsx delete mode 100644 ui/packages/platform/src/components/DBLabSettingsForm/DBLabSettingsFormWrapper.tsx delete mode 100644 ui/packages/platform/src/components/Dashboard/Dashboard.tsx delete mode 100644 ui/packages/platform/src/components/Dashboard/DashboardWrapper.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/DockerInstance.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/SimpleInstance.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/streamLogs.ts delete mode 100644 ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceForm.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceFormSidebar.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceFormSlider.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceFormWrapper.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceForm/reducer/index.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceForm/utils/index.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/AnsibleInstance.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/DockerInstance.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabFormSteps/SetupStep.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallForm.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormSidebar.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormWrapper.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceInstallForm/reducer/index.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstanceInstallForm/utils/index.ts delete mode 100644 ui/packages/platform/src/components/DbLabInstances/DbLabInstances.tsx delete mode 100644 ui/packages/platform/src/components/DbLabInstances/DbLabInstancesWrapper.tsx delete mode 100644 ui/packages/platform/src/components/DbLabSession/DbLabSession.tsx delete mode 100644 ui/packages/platform/src/components/DbLabSession/DbLabSessionWrapper.tsx delete mode 100644 ui/packages/platform/src/components/DbLabSessions/DbLabSessions.tsx delete mode 100644 ui/packages/platform/src/components/DbLabSessions/DbLabSessionsWrapper.tsx delete mode 100644 ui/packages/platform/src/components/DbLabStatus/DbLabStatus.tsx delete mode 100644 ui/packages/platform/src/components/DbLabStatus/DbLabStatusWrapper.tsx delete mode 100644 ui/packages/platform/src/components/DisplayToken/DisplayToken.tsx delete mode 100644 ui/packages/platform/src/components/DisplayToken/DisplayTokenWrapper.tsx delete mode 100644 ui/packages/platform/src/components/Error/Error.tsx delete mode 100644 ui/packages/platform/src/components/Error/ErrorWrapper.tsx delete mode 100644 ui/packages/platform/src/components/ExplainVisualization/ExplainVisualization.tsx delete mode 100644 ui/packages/platform/src/components/ExplainVisualization/ExplainVisualizationWrapper.tsx delete mode 100644 ui/packages/platform/src/components/FlameGraph.tsx delete mode 100644 ui/packages/platform/src/components/Head/index.tsx delete mode 100644 ui/packages/platform/src/components/IndexPage/IndexPage.tsx delete mode 100644 ui/packages/platform/src/components/IndexPage/IndexPageWrapper.tsx delete mode 100644 ui/packages/platform/src/components/JoeConfig.tsx delete mode 100644 ui/packages/platform/src/components/JoeHistory/JoeHistory.tsx delete mode 100644 ui/packages/platform/src/components/JoeHistory/JoeHistoryWrapper.tsx delete mode 100644 ui/packages/platform/src/components/JoeInstanceForm/JoeInstanceForm.tsx delete mode 100644 ui/packages/platform/src/components/JoeInstanceForm/JoeInstanceFormWrapper.tsx delete mode 100644 ui/packages/platform/src/components/JoeInstances/JoeInstances.tsx delete mode 100644 ui/packages/platform/src/components/JoeInstances/JoeInstancesWrapper.tsx delete mode 100644 ui/packages/platform/src/components/KBStats/KBStats.tsx delete mode 100644 ui/packages/platform/src/components/KBStats/hooks.ts delete mode 100644 ui/packages/platform/src/components/LoginDialog/LoginDialog.tsx delete mode 100644 ui/packages/platform/src/components/LoginDialog/LoginDialogWrapper.tsx delete mode 100644 ui/packages/platform/src/components/Notification/Notification.tsx delete mode 100644 ui/packages/platform/src/components/Notification/NotificationWrapper.tsx delete mode 100644 ui/packages/platform/src/components/OrgForm/OrgForm.tsx delete mode 100644 ui/packages/platform/src/components/OrgForm/OrgFormWrapper.tsx delete mode 100644 ui/packages/platform/src/components/OrgMembers/OrgMembers.tsx delete mode 100644 ui/packages/platform/src/components/OrgMembers/OrgMembersWrapper.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusterForm/PostgresCluster.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusterForm/PostgresClusterSteps/index.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusterForm/PostgresClusterWrapper.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusterForm/reducer/index.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusterForm/utils/index.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterInstallForm.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterInstallFormSidebar.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterInstallWrapper.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterSteps/AnsibleInstance.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusterInstallForm/PostgresClusterSteps/DockerInstance.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusterInstallForm/reducer/index.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusterInstallForm/utils/index.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusters/PostgresClusters.tsx delete mode 100644 ui/packages/platform/src/components/PostgresClusters/PostgresClustersWrapper.tsx delete mode 100644 ui/packages/platform/src/components/ProductCard/ProductCard.tsx delete mode 100644 ui/packages/platform/src/components/ProductCard/ProductCardWrapper.tsx delete mode 100644 ui/packages/platform/src/components/Report/Report.tsx delete mode 100644 ui/packages/platform/src/components/Report/ReportWrapper.tsx delete mode 100644 ui/packages/platform/src/components/ReportFile/ReportFile.tsx delete mode 100644 ui/packages/platform/src/components/ReportFile/ReportFileWrapper.tsx delete mode 100644 ui/packages/platform/src/components/Reports/Reports.tsx delete mode 100644 ui/packages/platform/src/components/Reports/ReportsWrapper.tsx delete mode 100644 ui/packages/platform/src/components/SIEMIntegrationForm/SIEMIntegrationForm.tsx delete mode 100644 ui/packages/platform/src/components/ShareUrlDialog/ShareUrlDialog.tsx delete mode 100644 ui/packages/platform/src/components/ShareUrlDialog/ShareUrlDialogWrapper.tsx delete mode 100644 ui/packages/platform/src/components/SharedUrl/SharedUrl.tsx delete mode 100644 ui/packages/platform/src/components/SharedUrl/SharedUrlWrapper.tsx delete mode 100644 ui/packages/platform/src/components/SideNav/index.tsx delete mode 100644 ui/packages/platform/src/components/StripeForm/index.tsx delete mode 100644 ui/packages/platform/src/components/StripeForm/stripeStyles.tsx delete mode 100644 ui/packages/platform/src/components/Warning/Warning.tsx delete mode 100644 ui/packages/platform/src/components/Warning/WarningWrapper.tsx delete mode 100644 ui/packages/platform/src/components/types/index.ts delete mode 100644 ui/packages/platform/src/config/emoji.ts delete mode 100644 ui/packages/platform/src/config/env.ts delete mode 100644 ui/packages/platform/src/config/routes/clones.ts delete mode 100644 ui/packages/platform/src/config/routes/index.ts delete mode 100644 ui/packages/platform/src/config/routes/instances.ts delete mode 100644 ui/packages/platform/src/helpers/localStorage.ts delete mode 100644 ui/packages/platform/src/helpers/request.ts delete mode 100644 ui/packages/platform/src/helpers/simpleInstallRequest.ts delete mode 100644 ui/packages/platform/src/hooks/useCloudProvider.ts delete mode 100644 ui/packages/platform/src/hooks/usePrev.ts delete mode 100644 ui/packages/platform/src/index.scss delete mode 100644 ui/packages/platform/src/index.tsx delete mode 100644 ui/packages/platform/src/meta.json delete mode 100644 ui/packages/platform/src/pages/Bot/BotWrapper.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/ChatsList/ChatsList.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Command/Command.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Command/useBuffer.ts delete mode 100644 ui/packages/platform/src/pages/Bot/Command/useCaret.ts delete mode 100644 ui/packages/platform/src/pages/Bot/Command/utils.ts delete mode 100644 ui/packages/platform/src/pages/Bot/DebugConsole/DebugConsole.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/DebugDialog/DebugDialog.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/DebugLogs/DebugLogs.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/HeaderButtons/HeaderButtons.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/HintCards/ArrowGrowthIcon/ArrowGrowthIcon.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/HintCards/CommonTypeIcon/CommonTypeIcon.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/HintCards/HintCard/HintCard.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/HintCards/HintCards.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/HintCards/TableIcon/TableIcon.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/HintCards/WrenchIcon/WrenchIcon.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/ErrorMessage/ErrorMessage.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/Message/CodeBlock/CodeBlock.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram/MermaidDiagram.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/Message/MermaidDiagram/MermaidDiagramControls.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/Message/Message.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/Message/MessageHeader/MessageHeader.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/Message/ThinkingCard/ThinkingCard.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/Message/ToolCallRenderer/ToolCallRenderer.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/Messages.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/Sources/SourceCard/SourceCard.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/Sources/SourcesFullList.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/Sources/SourcesShortList.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/Messages/utils.ts delete mode 100644 ui/packages/platform/src/pages/Bot/ModelSelector/ModelSelector.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/SettingsDialog/SettingsDialog.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/SettingsPanel/SettingsPanel.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/hints.ts delete mode 100644 ui/packages/platform/src/pages/Bot/hooks.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/index.tsx delete mode 100644 ui/packages/platform/src/pages/Bot/utils.ts delete mode 100644 ui/packages/platform/src/pages/Clone/index.tsx delete mode 100644 ui/packages/platform/src/pages/Consulting/ConsultingWrapper.tsx delete mode 100644 ui/packages/platform/src/pages/Consulting/TransactionsTable/TransactionsTable.tsx delete mode 100644 ui/packages/platform/src/pages/Consulting/index.tsx delete mode 100644 ui/packages/platform/src/pages/Consulting/utils.ts delete mode 100644 ui/packages/platform/src/pages/CreateClone/index.tsx delete mode 100644 ui/packages/platform/src/pages/Instance/index.tsx delete mode 100644 ui/packages/platform/src/pages/JoeInstance/Command/index.tsx delete mode 100644 ui/packages/platform/src/pages/JoeInstance/Command/useBuffer.ts delete mode 100644 ui/packages/platform/src/pages/JoeInstance/Command/useCaret.ts delete mode 100644 ui/packages/platform/src/pages/JoeInstance/Command/utils.ts delete mode 100644 ui/packages/platform/src/pages/JoeInstance/JoeInstanceWrapper.jsx delete mode 100644 ui/packages/platform/src/pages/JoeInstance/Messages/Banner/index.tsx delete mode 100644 ui/packages/platform/src/pages/JoeInstance/Messages/Banner/styles.module.scss delete mode 100644 ui/packages/platform/src/pages/JoeInstance/Messages/index.jsx delete mode 100644 ui/packages/platform/src/pages/JoeInstance/Messages/styles.module.scss delete mode 100644 ui/packages/platform/src/pages/JoeInstance/Messages/utils.ts delete mode 100644 ui/packages/platform/src/pages/JoeInstance/index.jsx delete mode 100644 ui/packages/platform/src/pages/JoeInstance/styles.scss delete mode 100644 ui/packages/platform/src/pages/JoeInstance/utils.ts delete mode 100644 ui/packages/platform/src/pages/JoeSessionCommand/JoeSessionCommandWrapper.jsx delete mode 100644 ui/packages/platform/src/pages/JoeSessionCommand/TabPanel/index.tsx delete mode 100644 ui/packages/platform/src/pages/JoeSessionCommand/index.js delete mode 100644 ui/packages/platform/src/pages/Profile/ProfileWrapper.tsx delete mode 100644 ui/packages/platform/src/pages/Profile/index.jsx delete mode 100644 ui/packages/platform/src/pages/SignIn/SignInWrapper.jsx delete mode 100644 ui/packages/platform/src/pages/SignIn/index.jsx delete mode 100644 ui/packages/platform/src/react-app-env.d.ts delete mode 100644 ui/packages/platform/src/react-syntax-highlighter.d.ts delete mode 100644 ui/packages/platform/src/registerServiceWorker.js delete mode 100644 ui/packages/platform/src/stores/app.ts delete mode 100644 ui/packages/platform/src/stores/banners.ts delete mode 100644 ui/packages/platform/src/stores/consulting.ts delete mode 100644 ui/packages/platform/src/stores/preformatJoeMessage.ts delete mode 100644 ui/packages/platform/src/stores/store.js delete mode 100644 ui/packages/platform/src/types/api/entities/bot.ts delete mode 100644 ui/packages/platform/src/types/api/entities/meta.ts delete mode 100644 ui/packages/platform/src/utils/aliases.ts delete mode 100644 ui/packages/platform/src/utils/cfggen.ts delete mode 100644 ui/packages/platform/src/utils/dblabutils.ts delete mode 100644 ui/packages/platform/src/utils/format.ts delete mode 100644 ui/packages/platform/src/utils/permissions.ts delete mode 100644 ui/packages/platform/src/utils/settings.ts delete mode 100644 ui/packages/platform/src/utils/time.ts delete mode 100644 ui/packages/platform/src/utils/urls.ts delete mode 100644 ui/packages/platform/src/utils/utils.ts delete mode 100644 ui/packages/platform/src/utils/webSockets.ts delete mode 100644 ui/packages/platform/tsconfig.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f32b4abf..0c4afb69 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -140,7 +140,6 @@ The [postgres-ai/database-lab](https://gitlab.com/postgres-ai/database-lab) repo - [Database Lab CLI](https://gitlab.com/postgres-ai/database-lab/-/tree/master/engine/cmd/cli) - [Database Lab UI](https://gitlab.com/postgres-ai/database-lab/-/tree/master/ui) - [Community Edition](https://gitlab.com/postgres-ai/database-lab/-/tree/master/ui/packages/ce) - - [Platform](https://gitlab.com/postgres-ai/database-lab/-/tree/master/ui/packages/platform) - [Shared components](https://gitlab.com/postgres-ai/database-lab/-/tree/master/ui/packages/shared) Components have a separate version, denoted by either: diff --git a/ui/.dockerignore b/ui/.dockerignore index 88026b98..7e3cab0d 100644 --- a/ui/.dockerignore +++ b/ui/.dockerignore @@ -7,4 +7,3 @@ ui/node_modules/ ui/packages/ce/node_modules/ ui/packages/shared/node_modules/ -ui/packages/platform/node_modules/ diff --git a/ui/.gitlab-ci.yml b/ui/.gitlab-ci.yml index c54ee265..22008ecf 100644 --- a/ui/.gitlab-ci.yml +++ b/ui/.gitlab-ci.yml @@ -1,6 +1,5 @@ include: - local: 'ui/packages/ce/.gitlab-ci.yml' - - local: 'ui/packages/platform/.gitlab-ci.yml' .ui_checks: &ui_checks rules: @@ -31,7 +30,6 @@ check-code-style: script: - pnpm --dir ui/ i - pnpm --dir ui/ --filter @postgres.ai/ce lint - - pnpm --dir ui/ --filter @postgres.ai/platform lint interruptible: true cache: <<: *cache diff --git a/ui/README.md b/ui/README.md index 9930d4ca..53d9a17d 100644 --- a/ui/README.md +++ b/ui/README.md @@ -1,4 +1,4 @@ -# Database Lab Engine UI and DBLab Platform UI +# Database Lab Engine UI ## DBLab - thin database clones and database branching for faster development @@ -15,7 +15,6 @@ As an example, cloning a 10 TiB PostgreSQL database can take less than 2 seconds ### List packages: -- `@postgres.ai/platform` - platform version of UI - `@postgres.ai/ce` - community edition version of UI - `@postgres.ai/shared` - common modules @@ -29,32 +28,15 @@ At the root: #### Examples - `npm ci -ws` - install deps of all packages - `npm run build -ws` - build all packages -- `npm run start -w @postgres.ai/platform` - run platform UI locally in dev mode - `npm run start -w @postgres.ai/ce` - run community edition UI locally in dev mode _Important note: don't use commands for `@postgres.ai/shared` - it's dependent package, which can't be running or built_ -### How to start Platform UI with a predefined JWT token -- `cd ui/packages/platform` -- `cp .env_example_dev .env` -- edit `.env` setting: - - `REACT_APP_API_URL_PREFIX` to point to dev API server (e.g., staging API server: `https://v2.postgres.ai/api/general`). - - `REACT_APP_TOKEN_DEBUG` to have your JWT ready work with the same server. Note that it has an expiration date so it needs to be periodically refreshed. -- `pnpm install` - to install packages if they are not found -- `pnpm run start` - start Platform for local debugging/development - ### How to start "ce" - `cd ui` - `npm ci -ws` - install dependencies, must be done once to install dependencies for all packages - `npm run start -w @postgres.ai/ce` - start dev server -### How to build "platform" - -- `cd ui` -- `npm ci -ws` - install dependencies, must be done once to install dependencies for all packages -- `source packages/platform/deploy/configs/production.sh` - set up environment variables, should be run for each new terminal session -- `npm run build -w @postgres.ai/platform` - ### How to build "ce" - `cd ui` @@ -91,5 +73,4 @@ Ways to resolve (ordered by preference): ## Moving to Typescript - `@postgres.ai/shared` is written on Typescript - `@postgres.ai/ce` is written on Typescript -- `@postgres.ai/platform` is written on JavaScript and patially on Typescript. The target - is moving `@postgres.ai/platform` to Typescript fully. It should takes approximately 120-160 hours. - There are potential problems with typing - old versions of packages may don't have their typings. Recommended to update them or replace. If it's impossible you can write your own typing in file named like `.d.ts` inside `src` directory of the selected package. diff --git a/ui/package.json b/ui/package.json index 82b2d887..9e92dbc2 100644 --- a/ui/package.json +++ b/ui/package.json @@ -7,7 +7,6 @@ }, "scripts": { "preinstall": "npx only-allow pnpm", - "start:platform": "source ./packages/platform/deploy/configs/production.sh && npm run start -w @postgres.ai/platform", "start:ce": "npm run start -w @postgres.ai/ce" }, "pnpm": { diff --git a/ui/packages/ce/.dockerignore b/ui/packages/ce/.dockerignore index 19d960ff..00dbf44f 100644 --- a/ui/packages/ce/.dockerignore +++ b/ui/packages/ce/.dockerignore @@ -6,5 +6,4 @@ **/build/** /ui/node_modules/ /ui/packages/ce/node_modules/ -/ui/packages/shared/node_modules/ -/ui/packages/platform/node_modules/ +/ui/packages/shared/node_modules/ \ No newline at end of file diff --git a/ui/packages/platform/.dockerignore b/ui/packages/platform/.dockerignore deleted file mode 100644 index 19d960ff..00000000 --- a/ui/packages/platform/.dockerignore +++ /dev/null @@ -1,10 +0,0 @@ -**/node_modules/** -/.vscode/ -/.idea/ -/bin/ -/.git/ -**/build/** -/ui/node_modules/ -/ui/packages/ce/node_modules/ -/ui/packages/shared/node_modules/ -/ui/packages/platform/node_modules/ diff --git a/ui/packages/platform/.env_example_dev b/ui/packages/platform/.env_example_dev deleted file mode 100644 index 53fc9fd8..00000000 --- a/ui/packages/platform/.env_example_dev +++ /dev/null @@ -1,3 +0,0 @@ -REACT_APP_API_URL_PREFIX=https://v2.postgres.ai/api/general -REACT_APP_TOKEN_DEBUG=__YOUR_JWT_TOKEN_FROM_STAGING__ -REACT_APP_WS_URL=ws://127.0.0.1:9100/ \ No newline at end of file diff --git a/ui/packages/platform/.eslintrc b/ui/packages/platform/.eslintrc deleted file mode 100644 index 4cc3d5ca..00000000 --- a/ui/packages/platform/.eslintrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "root": true, - "extends": "react-app", - "rules": { - "@typescript-eslint/no-explicit-any": "error" - } -} diff --git a/ui/packages/platform/.gitignore b/ui/packages/platform/.gitignore deleted file mode 100644 index 4c49bd78..00000000 --- a/ui/packages/platform/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.env diff --git a/ui/packages/platform/.gitlab-ci.yml b/ui/packages/platform/.gitlab-ci.yml deleted file mode 100644 index 79ca948e..00000000 --- a/ui/packages/platform/.gitlab-ci.yml +++ /dev/null @@ -1,131 +0,0 @@ -#-------------------------------------------------------------------------- -# Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai -# All Rights Reserved -# Unauthorized copying of this file, via any medium is strictly prohibited -# Proprietary and confidential -#-------------------------------------------------------------------------- - -# Conditions. -.only_ui_tag_release: &only_ui_tag_release - rules: - - if: $CI_COMMIT_TAG =~ /^ui\/[0-9.]+$/ - -.only_ui_staging: &only_ui_staging - rules: - - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' - changes: - - ui/packages/platform/**/* - - ui/packages/shared/**/* - -.only_ui_feature: &only_ui_feature - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - changes: - - ui/packages/platform/**/* - - ui/packages/shared/**/* - when: manual - -.ui_cache: &ui_cache - image: node:lts-alpine - cache: - key: "$CI_COMMIT_REF_SLUG" - paths: - - .pnpm-store - policy: pull - -# Environments. -.environment_production: &env_production - environment: - name: production - url: https://postgres.ai - variables: - ENV: production - NAMESPACE: production - DOCKER_NAME: "gcr.io/postgres-ai/platform-web/cloud" - before_script: - - export UI_VERSION=$(echo ${CI_COMMIT_TAG#"ui/"}) - - export TAG="${DOCKER_NAME}:${UI_VERSION}-${CI_PIPELINE_IID}" - -.environment_staging: &env_staging - environment: - name: staging - url: https://console-v2.postgres.ai - variables: - ENV: staging - NAMESPACE: staging - DOCKER_NAME: "gcr.io/postgres-ai/platform-web/cloud" - TAG: "${DOCKER_NAME}:${NAMESPACE}-${CI_PIPELINE_IID}" - -.environment_dev: &env_dev - environment: - name: dev - url: https://console-dev.postgres.ai - variables: - ENV: dev - NAMESPACE: dev - DOCKER_NAME: "gcr.io/postgres-ai/platform-web/cloud" - TAG: "${DOCKER_NAME}:${NAMESPACE}-${CI_PIPELINE_IID}" - -# Jobs templates. -.build_definition: &build_definition - <<: *ui_cache - stage: build - image: docker:20.10.12 - services: - - docker:dind - script: - - apk add --no-cache bash - - bash ./ui/packages/platform/ci_docker_build_push.sh - needs: - - job: check-code-style - artifacts: false - -.deploy_definition: &deploy_definition - stage: deploy - image: dtzar/helm-kubectl:2.14.1 - script: - # Substitute env variables in deploy config. - - bash ./ui/packages/platform/do.sh subs_envs ./ui/packages/platform/deploy/platform-console.yaml /tmp/platform-console.yaml - # Context - - kubectl config get-contexts - - kubectl config use-context postgres-ai/database-lab:k8s-cluster-1 - # Deploy to k8s cluster. - - kubectl apply --filename /tmp/platform-console.yaml -n $NAMESPACE - -# Jobs. -# Production. -ui_build_platform_image_tag_release: - <<: *env_production - <<: *only_ui_tag_release - <<: *build_definition - -ui_deploy_platform_image_tag_release: - <<: *env_production - <<: *only_ui_tag_release - <<: *deploy_definition - -# Staging. -ui_build_platform_image_staging: - <<: *env_staging - <<: *only_ui_staging - <<: *build_definition - -ui_deploy_platform_image_staging: - <<: *env_staging - <<: *only_ui_staging - <<: *deploy_definition - -# Dev. -ui_build_platform_image_dev: - <<: *env_dev - <<: *only_ui_feature - <<: *build_definition - allow_failure: true # Workaround: https://gitlab.com/gitlab-org/gitlab/-/issues/20237 - -ui_deploy_platform_image_dev: - <<: *env_dev - <<: *only_ui_feature - <<: *deploy_definition - allow_failure: true - needs: - - ui_build_platform_image_dev diff --git a/ui/packages/platform/.npmrc b/ui/packages/platform/.npmrc deleted file mode 100644 index 4c2f52b3..00000000 --- a/ui/packages/platform/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -auto-install-peers=true -strict-peer-dependencies=false diff --git a/ui/packages/platform/.stylelintrc b/ui/packages/platform/.stylelintrc deleted file mode 100644 index f623bb4d..00000000 --- a/ui/packages/platform/.stylelintrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "stylelint-config-sass-guidelines", - "rules": { - "selector-class-pattern": "^[a-z][a-zA-Z0-9]+$", - "order/order": null, - "order/properties-alphabetical-order": null - } -} diff --git a/ui/packages/platform/COPYRIGHT b/ui/packages/platform/COPYRIGHT deleted file mode 100644 index f41576fe..00000000 --- a/ui/packages/platform/COPYRIGHT +++ /dev/null @@ -1,6 +0,0 @@ -------------------------------------------------------------------------- -Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai -All Rights Reserved -Unauthorized copying of this file, via any medium is strictly prohibited -Proprietary and confidential -------------------------------------------------------------------------- diff --git a/ui/packages/platform/Dockerfile b/ui/packages/platform/Dockerfile deleted file mode 100644 index 7367bfb0..00000000 --- a/ui/packages/platform/Dockerfile +++ /dev/null @@ -1,67 +0,0 @@ -#-------------------------------------------------------------------------- -# Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai -# All Rights Reserved -# Unauthorized copying of this file, via any medium is strictly prohibited -# Proprietary and confidential -#-------------------------------------------------------------------------- - -# Build phase. -FROM node:16.14-alpine as build - -WORKDIR /app - -COPY ./ui/ . - -# RUN --mount=type=bind,id=pnpm,source=.pnpm-store,target=/app/.pnpm-store - -ARG ARG_REACT_APP_API_SERVER -ENV REACT_APP_API_SERVER=$ARG_REACT_APP_API_SERVER - -ARG ARG_PUBLIC_URL -ENV PUBLIC_URL=$ARG_PUBLIC_URL - -ARG ARG_REACT_APP_SIGNIN_URL -ENV REACT_APP_SIGNIN_URL=$ARG_REACT_APP_SIGNIN_URL - -ARG ARG_REACT_APP_AUTH_URL -ENV REACT_APP_AUTH_URL=$ARG_REACT_APP_AUTH_URL - -ARG ARG_REACT_APP_ROOT_URL -ENV REACT_APP_ROOT_URL=$ARG_REACT_APP_ROOT_URL - -ARG ARG_REACT_APP_WS_SERVER -ENV REACT_APP_WS_SERVER=$ARG_REACT_APP_WS_SERVER - -ARG ARG_REACT_APP_EXPLAIN_DEPESZ_SERVER -ENV REACT_APP_EXPLAIN_DEPESZ_SERVER=$ARG_REACT_APP_EXPLAIN_DEPESZ_SERVER - -ARG ARG_REACT_APP_EXPLAIN_PEV2_SERVER -ENV REACT_APP_EXPLAIN_PEV2_SERVER=$ARG_REACT_APP_EXPLAIN_PEV2_SERVER - -ARG ARG_REACT_APP_STRIPE_API_KEY -ENV REACT_APP_STRIPE_API_KEY=$ARG_REACT_APP_STRIPE_API_KEY - -ARG ARG_REACT_APP_SENTRY_DSN -ENV REACT_APP_SENTRY_DSN=$ARG_REACT_APP_SENTRY_DSN - -ARG ARG_REACT_APP_WS_URL -ENV REACT_APP_WS_URL=$ARG_REACT_APP_WS_URL - -ARG ARG_REACT_APP_BOT_API_URL -ENV REACT_APP_BOT_API_URL=$ARG_REACT_APP_BOT_API_URL - -RUN apk add --no-cache --update git && \ - npm i -g pnpm@7.30.5; \ - pnpm config set store-dir /app/.pnpm-store; \ - pnpm set verify-store-integrity false; \ - pnpm --filter @postgres.ai/platform i; \ - pnpm --filter @postgres.ai/platform build - -# Run phase. -FROM nginx:1.20.1-alpine as run - -COPY --from=build /app/packages/platform/build /srv/platform -COPY ./ui/packages/platform/nginx.conf /etc/nginx/conf.d/platform.conf -RUN rm -rf /etc/nginx/conf.d/default.conf - -CMD ["nginx", "-g", "daemon off;"] diff --git a/ui/packages/platform/README.md b/ui/packages/platform/README.md deleted file mode 100644 index 3e1e6cbd..00000000 --- a/ui/packages/platform/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# platform-console - -To start: -Specify REST API server URL with `REACT_APP_API_SERVER` environment variable. - -``` -npm install -npm run build -npm run start -``` - -# Q&A - -## `meta.json` is missing, what to do? -Run `npm run build`. - - \ No newline at end of file diff --git a/ui/packages/platform/ci_docker_build_push.sh b/ui/packages/platform/ci_docker_build_push.sh deleted file mode 100644 index b18aec0b..00000000 --- a/ui/packages/platform/ci_docker_build_push.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -docker_file=${DOCKER_FILE:-""} -tags=${TAG:-""} - -# Docker login for GCP. -echo $GCP_SERVICE_ACCOUNT | base64 -d > ./key.json -docker login -u _json_key --password-stdin https://gcr.io < ./key.json - -tags_build="" -tags_push="" - -IFS=',' read -ra ADDR string < $2 -} - -is_command_defined() { - type $1 2>/dev/null | grep -q 'is a function' -} - -# Parse command and arguments. -COMMAND=$1 -shift -ARGUMENTS=${@} - -# Run command. -is_command_defined $COMMAND -if [ $? -eq 0 ]; then - $COMMAND $ARGUMENTS -else - echo "Command not found" -fi diff --git a/ui/packages/platform/nginx.conf b/ui/packages/platform/nginx.conf deleted file mode 100644 index 3b65a273..00000000 --- a/ui/packages/platform/nginx.conf +++ /dev/null @@ -1,49 +0,0 @@ -server { - listen 3000; - server_name localhost; - root /srv/platform; - - # X-Frame-Options is to prevent from clickJacking attack. - # Makes impossible to use website in iframe. - add_header X-Frame-Options SAMEORIGIN; - - # Disable content-type sniffing on some browsers. - # Handle files strictly according to their mime types. - add_header X-Content-Type-Options nosniff; - - # Disable sending refferer to the downgraded security level. - # Example: https -> http. - add_header Referrer-Policy 'no-referrer-when-downgrade'; - - # Enable gzip compression only for static files. - gzip_static on; - - # Enables response header of "Vary: Accept-Encoding". - # It allows to serve both versions: compressed and not. - gzip_vary on; - - location / { - # No-cache doesn’t mean “don’t cache”, it means it must revalidate with the server before using the cached resource. - add_header Cache-Control 'no-cache'; - - # Enable entity tag to revalidate cache. - etag on; - - # Serve files. - try_files $uri $uri/ /index.html; - } - - location /static { - # This content can be cached as by user as by CDN's. - add_header Cache-Control 'public'; - - # Cache will be fresh for next 1 year. - expires 1y; - - # Disable logging static files requests. - access_log off; - - # Serve files. - try_files $uri $uri/; - } -} diff --git a/ui/packages/platform/package.json b/ui/packages/platform/package.json deleted file mode 100644 index 71ef15df..00000000 --- a/ui/packages/platform/package.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "name": "@postgres.ai/platform", - "version": "1.0.0", - "private": true, - "dependencies": { - "@craco/craco": "^6.4.3", - "@emotion/cache": "^11.10.5", - "@emotion/react": "^11.10.5", - "@emotion/server": "^11.10.0", - "@emotion/styled": "^11.10.5", - "@juggle/resize-observer": "^3.3.1", - "@material-ui/core": "^4.12.3", - "@material-ui/icons": "^4.11.2", - "@material-ui/lab": "4.0.0-alpha.61", - "@material-ui/styles": "^4.11.4", - "@material-ui/system": "^4.12.2", - "@monaco-editor/react": "^4.4.5", - "@mui/material": "^5.10.12", - "@postgres.ai/ce": "link:../ce", - "@postgres.ai/platform": "link:./", - "@postgres.ai/shared": "link:../shared", - "@sentry/react": "^6.11.0", - "@sentry/tracing": "^6.11.0", - "@stripe/react-stripe-js": "^1.1.2", - "@stripe/stripe-js": "^1.9.0", - "@types/d3": "^7.4.0", - "@types/dompurify": "^2.3.4", - "@types/node": "^12.20.33", - "@types/qs": "^6.9.7", - "@types/react": "^17.0.5", - "@types/react-dom": "^17.0.3", - "@types/react-router": "^5.1.17", - "@types/react-router-dom": "^5.1.7", - "@types/react-syntax-highlighter": "^15.5.6", - "bootstrap": "^4.3.1", - "byte-size": "^7.0.1", - "classnames": "^2.3.1", - "clsx": "^1.1.1", - "copy-to-clipboard": "^3.3.1", - "create-file-webpack": "^1.0.2", - "crypto-browserify": "^3.12.0", - "d3": "^5.12.0", - "d3-flame-graph": "^2.1.3", - "date-fns": "^2.22.1", - "dompurify": "^2.0.12", - "dotenv": "^10.0.0", - "es6-promise": "^4.2.8", - "formik": "^2.2.9", - "get-user-locale": "^1.4.0", - "jwt-decode": "^3.1.2", - "jwt-encode": "^1.0.1", - "lodash": "^4.17.15", - "md5": "^2.2.1", - "mermaid": "^11.0.2", - "mobx": "^6.3.2", - "mobx-react-lite": "^3.2.0", - "moment": "^2.24.0", - "postgres-interval": "^4.0.2", - "prop-types": "^15.7.2", - "qs": "^6.11.0", - "react": "^17.0.2", - "react-bootstrap": "^0.32.4", - "react-countdown-hook": "^1.1.0", - "react-div-100vh": "^0.6.0", - "react-dom": "^17.0.2", - "react-markdown": "^8.0.1", - "react-router": "^5.1.2", - "react-router-dom": "^5.1.2", - "react-router-hash-link": "^1.2.2", - "react-scripts": "^5.0.0", - "react-syntax-highlighter": "^15.5.0", - "react-use-websocket": "3.0.0", - "reflux": "^6.4.1", - "rehype-raw": "^6.1.1", - "remark-gfm": "^3.0.1", - "stream-browserify": "^3.0.0", - "typeface-roboto": "0.0.75", - "typescript": "^4.4.4", - "use-interval": "^1.3.0", - "use-timer": "^2.0.1", - "uuid": "^3.3.2", - "whatwg-fetch": "^3.6.2", - "yup": "^0.32.11" - }, - "scripts": { - "start": "craco start", - "build": "craco build", - "test": "craco test", - "eject": "craco eject", - "lint": "npm run lint:code && npm run lint:styles && npm run lint:spelling", - "lint:code": "eslint './src'", - "lint:styles": "stylelint \"src/**/*.scss\"", - "lint:spelling": "cspell './src/**/*' --no-progress --no-summary" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "devDependencies": { - "@babel/core": "^7.19.0", - "@babel/eslint-parser": "^7.18.9", - "@babel/eslint-plugin": "^7.18.10", - "@babel/preset-react": "^7.18.6", - "@tsconfig/recommended": "^1.0.1", - "@typescript-eslint/eslint-plugin": "^5.6.0", - "@typescript-eslint/parser": "^5.6.0", - "cspell": "^5.6.6", - "eslint": "^8.23.0", - "eslint-plugin-react": "^7.18.0", - "eslint-plugin-react-hooks": "^4.2.0", - "sass": "^1.37.5", - "stylelint": "^13.13.1", - "stylelint-config-sass-guidelines": "^8.0.0" - } -} diff --git a/ui/packages/platform/public/auth-gate.html b/ui/packages/platform/public/auth-gate.html deleted file mode 100644 index 9621ad54..00000000 --- a/ui/packages/platform/public/auth-gate.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - Postgres.ai - - - - - - diff --git a/ui/packages/platform/public/favicon.ico b/ui/packages/platform/public/favicon.ico deleted file mode 100644 index 808c8a49dceb8edb4111bcc203b64c662fd51453..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8490 zcmeI1X>3$g6vv-_@S`6(TW9Gkeecb5A6iNm+Oo8?9m-yyrIgZ&3*x@SHGUw*XrhUU zYcvXqM$xEXH1UfXi4ir%7y$#g1W_?A;0DG8N{{D%XYT8LGo3Q6RXd4o-rP55-n-{_ z&pqedbKldnv-m$=uJLDBb4E1Ht!Y{bA8|f<_<$UL_lGo1uSy<5koI91MorW-w?|Fm z2p=?!j-xTtc-}P4`ZQ$mHD(wanDyi|;vLI&rDAVfM4x<{kOPos7d0!{{_kV@p1=bL?(q?)~}5p5p!`PuanJdUEV0nI$u1 z?Ee?-2y^4es@p7Ypit9wU^x%O*v44tBa^Jw%K{DWL;J}1wL-Q;h7gu?Zk zcwF(UeU7JZg1?Q)9+|m@3YWi01w*Gt8CBjhoI3{pB;TSJNv~?RjUm`_3mFwHHd(m- zLMpCVX_FPtSWKbD%gBtzZ1S|RWv~a@?qlAONio~zgU5$4gc`53^t<{FQNiHv6r8i! zlKDFy7rMNQc9Iz@6LQQQ`f{!Qi1f;N*0tlY8|95t#x2L;?El`iaU|a10#JdY@zcua;hw9WewB zU*ZYtUf0?qLMH7lbsH6PtUhvOclCZsfwp@I{%@4liu;jYjMACb5l#7|QQkz6x*>uO zIM;q-@u=+X-h-ChpLk02=kMHN$&d@c7OvZ1$-u8y%*~m-aOrELS1%K>Ry6Mp@-BIW zZJqR0X$!S=uKSVPtM`&`;dahT*Ymh$3H|O>dq`zB%jziDd?Pv6{Xml4jFnTQwx4jV zWH(Ey$cVR#c+ZvHEUV|3U&Zlt75mwK+t^jsGo2%k7A||8imR7%&b-L7nXC5`!Bbqb zf{Nzdmb81vR*_#LtT)j9aMJD^tC+iB^UYMmah;3Zh5vOuH)-aKF%1noNEsAIyOA!y^g`(q^@Mlq-^0!JT2sE zo{|2p+8z4!%K26w${4bpv4QnavKtl6B6s56iLg7E*~dN)M)Zps1+~n0IUN0C4Jusr zhG5s@2@$9GuCn_VJizjmBJWCe8oSM@($$ICMXqE0`Q%rQK_sMq0($UCS* zGU|HP3B9Vn!!IPKWJhev9EDm5;Cy-J;#{Ml1foS1e>lQ@1h-C3!KPcM@@yA$JP58_npj3}8;yB()T5x|Y0)lYL5iWo=mp>Q!IYGh%&2y%v~%C!zLKea6$hn`3f`&;dDo z2I~XtAlG2{JMtQ0CETzvrcXGoJ3ZEkDb~H4(YL<55euWXJu11rMdM^FD)XQE;j3 zShFyPY25g3W5+d9wcnHDlluwiR@p{-OxF{5 zZ^Q3C_T?47VeADr-k0IM8M6=P3BUTD5BM6tU)_`k{KNcy^_QCJ`8lr}=lgDsu xn$5vcUUPSVPeneAFXw97F0|)p+Ge!ElW_9?- -Ansible icon \ No newline at end of file diff --git a/ui/packages/platform/public/images/avatar.jpg b/ui/packages/platform/public/images/avatar.jpg deleted file mode 100644 index deae82ed059509399e033d18e74f08c6d5dff200..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62337 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

cUB%z8`mLl`x@0@+Max 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

OVH@&n(Dek@%@)=+xW>v7_O@j%z9ZjH-4Z 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

K{UseFn?P54(h){MtvFua87r3@X zjLKyF@%>`otU)x0^R;(3@b*1mPU`bVvo>PZNJocfD2+REwik~A*jUj0T*|o? zIlq)uObH;rItNZgZ=DLCa8HsVy?jM@=pK@ZBmc~g-Z@&BDryOOct;bJD07}tkiy=G zGR+S(Qq^!qm9wL}l7tOn=@>xJCDrgE@J~|7;1XUX0;lt0E^2i?Eb!4c$MKB$YrJY& zf)V*0zN(wI?5a}myGl+^D;qBo#jpxq{)6WWtq3Y(M^~O-#X5CLuk2$LCy8Qt-{NbWYe1MDex3JO|BYt3&QE%< zSnbJhjaF}v8VIzTMOT+3!z2)V+JtpA<`Rv$VE4Ayhcnn6I9LBMbp5> zXI>0?#$lhlhSb;B4?-heXu8Tx7$@|MNfEAc8=5D_F;T2^UTSOloznN^M>3c=uL0bi z9jsL@hUz&QGy(z|dUmGqjb3R1bwGFR>cuK*$iCO$b9s5X85pOEW)m;TBto)9 zKw$o60yc&9&Fd0uZg{YJ&NrI!`nfN|KoRzn!vcq~uSs(tuJrdW7*4$azLjtI1UQ8L zP88Dlc)?zxNQvRfHWOKi0~V0fzwzNwU!t5zA|!t7e}&8V(D#R7$fUn~N%;q3%^je9 z{F06OodXr+*|~A_4dR4U;|OeEO>Q5QHFI`G&2i4tSK*dmY=e^%EtMKk{j@#L4$C zuTNkRz+YUO8f$2x9N1+J;-Ex_>{fha6cr_S8#+(cqK{56OEY9SfJ{GFl4|Ja*z26Q zq5FuEPj!0sxAPDjO(lpgEa9WpVZJE7ziAXO;)N&5`FH1c6p438 zo)KpnawD$HXY%+np1RPh=2uN2Y4=eWUGz+)f*`4|fYVoynceuvs9cs1k?<}buw2G^ z<(qbpC6qVnaJjj*t!+6FrR1I#rV4H(>4~>;R8Bz0TDvUzC4Nx?n6?)b7RIf|eust3 zjd~T1PZGTDxHFW7M zn%qq+20%kk=pcci3(mj>#ZBj>c~}?7m%h?V2f&^OryRb{O1#b&!4FdPq1X4#1hiF4Qmd3p zb{(N-q(CINVS6_s4*CN+zxMUWrrH92FdseDBM(!qxT4=mxL#u+0Y6a$mDk+dfUC@g zpIZWuR2vqs)X=Z>0RNd1aFW8V4zP0Qs(u885ewB#0ND;)^Gl}kng-jcMTpEb28hH{ zE2zw{pvbYLBD#AhpgB#0r30yDKB>cRO0)I%Yu9ncEjX`7>_4MzK)@QVh++nFN0P=Ei+G2@%i0m>~>ATPW>9Yt_yr;mH zD4R5EwEX=J`p^hubOv0BwuYe}3v~-&cq6BU#o)98nOy5dhIz1q0tlf(;D&g1Tvg=Z{@A#@)W|8lj9Pi0!0MCRi{=w73L(=!tCH?ko*H zK^HRYOvXfSn%ZQ6OlHjqkOFyw?K_R^y#o5(gt_Yl=KzRVIT^RBubJbfIk=>#oxNS7%1yAT+%A>=v||I|OAX=rGqsO#lFEtN0>l=kBz zW2Ti{ZzC8G!(#!Dxjk*2DEUijRC0@mJ_q2>dn$A`Pp~But*pf1Uaua>`U&?6gg`6= z10cyuPbV$F4`h*}{46u1N3T;M{&d)%ifKzAf}!Bt_m}y$aUdo3<9aE&F(5xZu^gF@ ziy~s+BZHQD=)b19>kT-+kpd!EPTi47ENYB7(hfDc^BvV3N*-DHo+s*2grp-VmdW zAhLQiFNmQ1BrH9A%0eh{osU7e+d||F}%QF_)R9;wlp`_ZoN=r zwbFw0gdG)3D6^dDCCG-iq9r&e(jOwn7t1Gqb(g1oFEhLIScbEW`SND8hs^k0W;(f zYMq1AtbpFEq54fn)iZSUcy+wqk5Lj#;ci3NO8*`#o6vk1e$(qVtHS0Oo+u<+jGG9N zxrSUrkRJmR=?n+-dL$VvdPXQem;{M>%671#8zUJZA1c`AvLt-jWZLduUcN^u*rzPd z2J)$X;jJU(^+-u7?Q~q)f^fN$Njg=84s=24;OZZj)Wz}LhjaJ4muw%T7=BtUoVqOW w`e7p-HaHK$l1Z!Qm&QN7mH)rL@GY=yigZ{7&CDPeWCMVTf~I_xtmXUv2i}`=&j0`b diff --git a/ui/packages/platform/public/images/paymentMethods/visa.png b/ui/packages/platform/public/images/paymentMethods/visa.png deleted file mode 100644 index 7d21c22cd32cf522c50aae92686353e2f6150656..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5167 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 diff --git a/ui/packages/platform/public/images/service-providers/digitalocean.png b/ui/packages/platform/public/images/service-providers/digitalocean.png deleted file mode 100644 index 3df53ee4e7c2b53f00fd7a264d5d63128857c6ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6712 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 O0000@&=w00009a7bBm000XU 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 - - - - Svg Vector Icons : http://www.onlinewebfonts.com/icon - - \ No newline at end of file diff --git a/ui/packages/platform/public/images/warning.png b/ui/packages/platform/public/images/warning.png deleted file mode 100644 index cd747ffe14f882246ce4a3abd78812a174864bea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1906 zcmV-&2aWiNP)_-Ys-t5V|b7pp)=Xu|G zXBIf_@jreafHwU7J(+sy!nvW7f1Z7Qj_kruLnkhtAKrFP0Fe1PwN=>i3}fo0WCC$A z#?{LUi}@e_nS9}iRS!VUwm-2f#5WBB>(?r6^lVv30kJ`v56h>Ah0K~p+%OPT#G3D}8TRT+1JAwUns(^O_ z`|VT#?-cgir2^h5?6*q=ypz~(hYENnvEL3A@J?gD)hgiaU|%U<{w~9fS!499RC`pw zTg^T#Hgk#N(ll`OKV$SvZRBgU3V18ozcbHp`3i7(n&HS-77l#WrR}q?L%2K*?V~=e zQ~_@l`${ofxk`&XsV!@?ppsbxlON5=q>SD2qy#rJ3o8cRtuMFoKaAS6aDu8@o`ErKkc zCm8^L{N-MJ!N^B_aycw}m&T$og;XOQ4M1jc$FOayp@F0VW74d~F_Q}vI9Fu>DD)|O zF1;&3V;U9gNOvp%!nQowC*#hd60!2{8=ql&<@-ey*umxk9xVHEXrNCni~WKX78Q%g zvI-zy5v&#On_M8FBuh&Mu<~>F;asdyHeHivv#D2jn z%hUt$D0~!P`f%<&hRy|~0;&rpo=bNO03zqlerX6q!+r`_-q%+JHXrE-05TI!>?dr` z*k1|(^BM62AnYFk#axiEp`;2#1-$XhFDtTNNNk(|v2gjZqG9Mh4E|#kRUjJQ5qQJw zORD+H$NtJZ5DhCI1r&0HznQu~D=9TK@l3jH00<#QNv_G5^kr;G1riZo004_uHx|uF zbBkPKxu9+UGULyhs;|tx@0kdhLZr7m0Fho<%oUZ4=Il?93eb=W>IOh4C-$pkzmNxr zUKQ2u!$km?U}b_WkPb`si32_ESLE^r_|UP^&;!hxMs|w~Boe0uqt# z@_>cAmMX@YRj^o}3V7}8KRs-V%KLt~5TGpI+v8?G++%#Tgg`MD#GSfeyml4v+A}|D z%KYwX5Qux62jWqweVnP9$JHvJ9YM&ErUIbZmz4SKs`h!O z1Zy|?PW|ybl$5Ed@h8)P10ZbY#22&cVyI9FnjGLBUx$FIz7q7^5f-Y?1-xqZDfZ*F zXZ}(PadYM6bt@E$IM-oJ)jfW3N1*EP^wxA>00<$S*sq`c!WX1cH|kLg#hfyjK{Yf- zf6R7vgz9dMDgux>wZ%E_=j)FBQll^+xqIybM*fXiF&EgXDi!d`Ri6|4(Z<*>tpy@I z((@ox?+)Op0$$yYz$>yZDE37|v0rN4A&H09n>xPq_kh853SCw(bLdPwF z*A0NM)hNj_DE4&?5EfXQRCxcby#6FR@HVwU`+}Oss6bY{H7W~0=ES2W_5);J|1QvL zV@+HjwH-L}q0pAvu5=suf|{qO0y-o3sGuwW0#x-8`=a5*RBBSv524Gyta94O7vx+7 z0Ifmf#7Ip5X!ZpHXP<2Ps)e~b9N(N&cz2ZJy^lG*{a)!d@&!59k_uRA>f|HossS)J zy#@Az`$7wg40rBw9Q^_K^01uE6jQd(_?2Pg0KFv@8Rfirb;O^Mxrs@lr{bdG^pT?n@HeY{KnCj3&1>D8H zZ4V8^bAeQSD=pk@K;5%bx}>;gr`Z8apJ31;S8`aNl-lTd@32IySLi;?Q-O5d8u2Ce2c4=fA9(DS+Jgf*B*Fr3zNql| zNM0K~_eBIA=+8B6UXu&*=GN%ghO`R+VdJ2#`6Eq?edpf^46a$k#&ru=*GDSI>E{gt zi#6W}*QElkAO$<%0)TFMy_oGm_GTBZ&8{w^HT|3Ip(FBa2<)H> zfK9Jm`K>3!c0s8VH*Z;U*KdWenr!yAH4CYZcUyet!>>>O?pX&A2%Gn2_iTA1+qY%^ s3|me1`2K8i^SrHjY?|2+=;527g2Q^ - - - - - - - - - - - - - - - - - - - - - - - - - - - - Postgres.ai - - - - - - - -
- - diff --git a/ui/packages/platform/public/manifest.json b/ui/packages/platform/public/manifest.json deleted file mode 100644 index 56180c0d..00000000 --- a/ui/packages/platform/public/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "short_name": "Database Lab Platform", - "name": "Database Lab Platform", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - } - ], - "start_url": "./index.html", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/ui/packages/platform/src/App.jsx b/ui/packages/platform/src/App.jsx deleted file mode 100644 index a8859920..00000000 --- a/ui/packages/platform/src/App.jsx +++ /dev/null @@ -1,50 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component } from 'react' -import { loadStripe } from '@stripe/stripe-js' -import { Elements } from '@stripe/react-stripe-js' -import { BrowserRouter as Router, Route } from 'react-router-dom' -import { - createGenerateClassName, - StylesProvider, - ThemeProvider, -} from '@material-ui/core/styles' - -import { ROUTES } from 'config/routes' - -import { IndexPageWrapper } from 'components/IndexPage/IndexPageWrapper' -import { theme } from '@postgres.ai/shared/styles/theme' -import settings from 'utils/settings' - -const stripePromise = loadStripe(settings.stripeApiKey, { - locale: 'en', -}) - -class App extends Component { - render() { - const generateClassName = createGenerateClassName({ - productionPrefix: 'p', - }) - - return ( - - - - - - - - - - - - ) - } -} - -export default App diff --git a/ui/packages/platform/src/App.test.js b/ui/packages/platform/src/App.test.js deleted file mode 100644 index 076db353..00000000 --- a/ui/packages/platform/src/App.test.js +++ /dev/null @@ -1,17 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -/* eslint-disable */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); -}); diff --git a/ui/packages/platform/src/actions/actions.js b/ui/packages/platform/src/actions/actions.js deleted file mode 100644 index 210fa8fc..00000000 --- a/ui/packages/platform/src/actions/actions.js +++ /dev/null @@ -1,1760 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import Reflux from 'reflux'; - -import { localStorage } from 'helpers/localStorage'; - -import Api from '../api/api'; -import ExplainDepeszApi from '../api/explain/depesz'; -import ExplainPev2Api from '../api/explain/pev2'; -import settings from '../utils/settings'; -import { visualizeTypes } from '../assets/visualizeTypes'; - -let api; -let explainDepeszApi; -let explainPev2Api; - -settings.init(() => { - api = new Api(settings); - explainDepeszApi = new ExplainDepeszApi(settings); - explainPev2Api = new ExplainPev2Api(settings); -}); - -// Timeout: 30 sec. -const REQUEST_TIMEOUT = 30000; -const ASYNC_ACTION = { - asyncResult: true, - children: ['progressed', 'completed', 'failed'] -}; - -const Actions = Reflux.createActions([{ - auth: {}, - signOut: {}, - ASYNC_ACTION: ASYNC_ACTION, - doAuth: ASYNC_ACTION, - getUserProfile: ASYNC_ACTION, - updateUserProfile: ASYNC_ACTION, - getAccessTokens: ASYNC_ACTION, - getAccessToken: ASYNC_ACTION, - hideGeneratedAccessToken: {}, - revokeAccessToken: ASYNC_ACTION, - getCheckupReports: ASYNC_ACTION, - getCheckupReportFiles: ASYNC_ACTION, - getCheckupReportFile: ASYNC_ACTION, - getJoeSessions: ASYNC_ACTION, - getJoeSessionCommands: ASYNC_ACTION, - getJoeSessionCommand: ASYNC_ACTION, - getProjects: ASYNC_ACTION, - getOrgs: ASYNC_ACTION, - getOrgUsers: ASYNC_ACTION, - updateOrg: ASYNC_ACTION, - createOrg: ASYNC_ACTION, - updateAiBotSettings: ASYNC_ACTION, - updateAuditSettings: ASYNC_ACTION, - updateDBLabSettings: ASYNC_ACTION, - inviteUser: ASYNC_ACTION, - useDemoData: ASYNC_ACTION, - setReportsProject: {}, - setSessionsProject: {}, - setDbLabInstancesProject: {}, - refresh: {}, - getDbLabInstances: ASYNC_ACTION, - addDbLabInstance: ASYNC_ACTION, - editDbLabInstance: ASYNC_ACTION, - destroyDbLabInstance: ASYNC_ACTION, - resetNewDbLabInstance: {}, - getDbLabInstanceStatus: ASYNC_ACTION, - checkDbLabInstanceUrl: ASYNC_ACTION, - downloadReportJsonFiles: ASYNC_ACTION, - addOrgDomain: ASYNC_ACTION, - deleteOrgDomain: ASYNC_ACTION, - showNotification: {}, - hideNotification: {}, - setJoeInstancesProject: {}, - getJoeInstances: ASYNC_ACTION, - getJoeInstanceChannels: ASYNC_ACTION, - sendJoeInstanceCommand: ASYNC_ACTION, - initJoeWebSocketConnection: {}, - getJoeInstanceMessages: ASYNC_ACTION, - getJoeMessageArtifacts: ASYNC_ACTION, - resetNewJoeInstance: {}, - checkJoeInstanceUrl: ASYNC_ACTION, - addJoeInstance: ASYNC_ACTION, - destroyJoeInstance: ASYNC_ACTION, - closeJoeWebSocketConnection: {}, - getExternalVisualizationData: ASYNC_ACTION, - closeExternalVisualization: {}, - deleteJoeSessions: ASYNC_ACTION, - deleteJoeCommands: ASYNC_ACTION, - deleteCheckupReports: ASYNC_ACTION, - joeCommandFavorite: ASYNC_ACTION, - clearJoeInstanceChannelMessages: {}, - getSharedUrlData: ASYNC_ACTION, - getSharedUrl: ASYNC_ACTION, - addSharedUrl: ASYNC_ACTION, - removeSharedUrl: ASYNC_ACTION, - showShareUrlDialog: {}, - closeShareUrlDialog: {}, - getBillingDataUsage: ASYNC_ACTION, - subscribeBilling: ASYNC_ACTION, - setSubscriptionError: {}, - getDbLabInstance: ASYNC_ACTION, - getJoeInstance: ASYNC_ACTION, - updateOrgUser: ASYNC_ACTION, - deleteOrgUser: ASYNC_ACTION, - getDbLabSessions: ASYNC_ACTION, - getDbLabSession: ASYNC_ACTION, - getDbLabSessionLogs: ASYNC_ACTION, - getDbLabSessionArtifacts: ASYNC_ACTION, - getDbLabSessionArtifact: ASYNC_ACTION, - getAuditLog: ASYNC_ACTION, - downloadDblabSessionLog: ASYNC_ACTION, - downloadDblabSessionArtifact: ASYNC_ACTION, - sendUserCode: ASYNC_ACTION, - confirmUserEmail: ASYNC_ACTION, - confirmTosAgreement: ASYNC_ACTION, - testSiemServiceConnection: ASYNC_ACTION, - getAuditEvents: ASYNC_ACTION -}]); - -function timeoutPromise(ms, promise) { - return new Promise((resolve, reject) => { - const timeoutId = setTimeout(function () { - reject(new Error('timeout')); - }, ms); - - promise - .then( - (res) => { - clearTimeout(timeoutId); - resolve(res); - }, - (err) => { - clearTimeout(timeoutId); - reject(err); - }); - }); -} - -function actionResult(promise, cb, errorCb) { - timeoutPromise(REQUEST_TIMEOUT, promise) - .then(result => { - let count; - try { - let range = result.headers.get('content-range'); - if (range) { - range = range.split('/'); - if (Array.isArray(range) && range.length) { - range = range[range.length - 1]; - count = parseInt(range, 10); - } - } - } catch (e) { - console.log('Range is empty'); - } - - result.json() - .then(json => { - if (!json) { - if (errorCb) { - errorCb(new Error('wrong_reply')); - } else { - this.failed(new Error('wrong_reply')); - } - } - - if (cb) { - cb(json, count); - } else { - this.completed(json, count); - } - }) - .catch(err => { - console.error(err); - - if (errorCb) { - errorCb(new Error('wrong_reply')); - } else { - this.failed(new Error('wrong_reply')); - } - }); - }) - .catch(err => { - console.error(err); - let actionErr = new Error('wrong_reply'); - - if (err && err.message && err.message === 'timeout') { - actionErr = new Error('failed_fetch'); - } - - if (errorCb) { - errorCb(new Error(actionErr)); - } else { - this.failed(actionErr); - } - }); -} - -Actions.doAuth.listen(function (email, password) { - const action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - if (!email && !password) { - // Token should be passed through /auth.html page. - // Example: /auth-gate.html?token=some-token - const token = localStorage.getAuthToken(); - - if (token) { - action.completed({ token: token }); - } else { - action.failed(new Error('empty_request')); - } - - return; - } - - action.progressed(); - - timeoutPromise(REQUEST_TIMEOUT, api.login(email, password)) - .then(result => { - if (result.status !== 200) { - action.failed(new Error('wrong_reply')); - } - - result.json() - .then(json => { - if (json) { - if (typeof json === 'object') { - action.failed(new Error(json.message)); - } else { - action.completed({ token: json }); - } - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.getUserProfile.listen(function (token) { - this.progressed(); - - actionResult.bind(this)( - api.getUserProfile(token), - (json) => { - this.completed(json); - } - ); -}); - -Actions.updateUserProfile.listen(function (token, data) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - this.progressed(); - - timeoutPromise(REQUEST_TIMEOUT, api.updateUserProfile(token, data)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed({ data: json }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.getAccessTokens.listen(function (token, orgId) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed(); - - timeoutPromise(REQUEST_TIMEOUT, api.getAccessTokens(token, orgId)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed({ data: json, orgId: orgId }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.getAccessToken.listen(function (token, name, expires, orgId, isPersonal) { - let requestExpires = expires.split('-').join('') + 'T235959-0330'; - - this.progressed(); - actionResult.bind(this)( - api.getAccessToken(token, name, requestExpires, orgId, isPersonal), - (json) => { - this.completed({ orgId: orgId, data: json }); - }); -}); - -Actions.revokeAccessToken.listen(function (token, orgId, id) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed(id); - - timeoutPromise(REQUEST_TIMEOUT, api.revokeAccessToken(token, id)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed({ orgId: orgId, data: json }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.getCheckupReports.listen(function (token, orgId, projectId, reportId) { - this.progressed(); - - actionResult.bind(this)( - api.getCheckupReports(token, orgId, projectId, reportId), - (json) => { - this.completed({ - data: json, - orgId: orgId, - projectId: projectId, - reportId: reportId - }); - } - ); -}); - -Actions.getCheckupReportFiles.listen(function (token, reportId, type, orderBy, - orderDirection) { - this.progressed(); - - actionResult.bind(this)( - api.getCheckupReportFiles(token, reportId, type, orderBy, orderDirection), - (json) => { - this.completed({ reportId: reportId, data: json, type: type }); - } - ); -}); - -Actions.getCheckupReportFile.listen(function (token, projectId, reportId, fileId, type) { - this.progressed(); - - actionResult.bind(this)( - api.getCheckupReportFile(token, projectId, reportId, fileId, type), - (json) => { - this.completed(fileId, json); - } - ); -}); - -Actions.getJoeSessions.listen(function (token, orgId, projectId) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed(); - - timeoutPromise(REQUEST_TIMEOUT, api.getJoeSessions(token, orgId, projectId)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed({ data: json, orgId: orgId, projectId: projectId }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.getJoeSessionCommands.listen(function (token, params) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed(params); - - timeoutPromise(REQUEST_TIMEOUT, api.getJoeSessionCommands(token, params)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed(json, params); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.getJoeSessionCommand.listen(function (token, orgId, commandId) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed(); - - timeoutPromise(REQUEST_TIMEOUT, api.getJoeSessionCommand(token, orgId, commandId)) - - .then(result => { - result.json() - .then(json => { - if (json) { - const command = json[0]; - action.completed(command); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.getProjects.listen(function (token, orgId) { - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - let action = this; - - action.progressed(); - - timeoutPromise(REQUEST_TIMEOUT, api.getProjects(token, orgId)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed({ data: json, orgId: orgId }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.getOrgs.listen(function (token, orgId) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed({ orgId }); - - timeoutPromise(REQUEST_TIMEOUT, api.getOrgs(token, orgId)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed({ data: json, orgId: orgId }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.getOrgUsers.listen(function (token, orgId) { - this.progressed(orgId); - - actionResult.bind(this)( - api.getOrgUsers(token, orgId), - (json) => { - this.completed(orgId, json); - }, - (err) => { - this.failed(orgId, err); - } - ); -}); - -Actions.updateOrg.listen(function (token, orgId, orgData) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed({ orgId } + orgData); - timeoutPromise(REQUEST_TIMEOUT, api.updateOrg(token, orgId, orgData)) - - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed(json); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.updateAiBotSettings.listen(function (token, orgId, orgData) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed({ orgId } + orgData); - timeoutPromise(REQUEST_TIMEOUT, api.updateAiBotSettings(token, orgId, orgData)) - - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed(json); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.updateAuditSettings.listen(function (token, orgId, orgData) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed({ orgId } + orgData); - timeoutPromise(REQUEST_TIMEOUT, api.updateAuditSettings(token, orgId, orgData)) - - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed(json); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.updateDBLabSettings.listen(function (token, orgId, orgData) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed({ orgId } + orgData); - timeoutPromise(REQUEST_TIMEOUT, api.updateDBLabSettings(token, orgId, orgData)) - - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed(json); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.createOrg.listen(function (token, orgData) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed(orgData); - timeoutPromise(REQUEST_TIMEOUT, api.createOrg(token, orgData)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed(json); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.inviteUser.listen(function (token, orgId, email) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed({ orgId, email }); - timeoutPromise(REQUEST_TIMEOUT, api.inviteUser(token, orgId, email)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed(json); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.useDemoData.listen(function (token) { - this.progressed(); - - actionResult.bind(this)( - api.useDemoData(token), - (json) => { - this.completed(json); - }, - (err) => { - this.failed(err); - } - ); -}); - -Actions.getDbLabInstances.listen(function (token, orgId, projectId) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed(); - timeoutPromise(REQUEST_TIMEOUT, api.getDbLabInstances(token, orgId, projectId)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed({ data: json, orgId: orgId, projectId: projectId }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.addDbLabInstance.listen(function (token, instanceData) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed(); - - timeoutPromise(REQUEST_TIMEOUT, api.addDbLabInstance(token, instanceData)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed( - { data: json, orgId: instanceData.orgId, project: instanceData.project }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.editDbLabInstance.listen(function (token, instanceData) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed(); - - timeoutPromise(REQUEST_TIMEOUT, api.editDbLabInstance(token, instanceData)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed( - { data: json, orgId: instanceData.orgId, project: instanceData.project }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.destroyDbLabInstance.listen(function (token, instanceId) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed({ instanceId: instanceId }); - - timeoutPromise(REQUEST_TIMEOUT, api.destroyDbLabInstance(token, instanceId)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed({ data: json, instanceId: instanceId }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.getDbLabInstanceStatus.listen(function (token, instanceId) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed({ instanceId: instanceId }); - - timeoutPromise(REQUEST_TIMEOUT, api.getDbLabInstanceStatus(token, instanceId)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed({ data: json, instanceId: instanceId }); - } else { - action.failed({ instanceId: instanceId }, new Error( - 'wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed({ instanceId: instanceId }, new Error( - 'wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed({ instanceId: instanceId }, new Error( - 'failed_fetch')); - } else { - action.failed({ instanceId: instanceId }, new Error( - 'wrong_reply')); - } - }); -}); - -Actions.checkDbLabInstanceUrl.listen(function (token, url, verifyToken, useTunnel) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed(); - - timeoutPromise(REQUEST_TIMEOUT, api.checkDbLabInstanceUrl(token, url, verifyToken, useTunnel)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed(json); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.downloadBinaryFile = (action, token, fileUrl, storeParams = {}) => { - let xhr = new XMLHttpRequest(); - - action.progressed(storeParams); - xhr.open('GET', fileUrl); - xhr.setRequestHeader('authorization', 'Bearer ' + token); - xhr.setRequestHeader('accept', 'application/octet-stream'); - xhr.responseType = 'arraybuffer'; - xhr.addEventListener('readystatechange', function () { - if (this.readyState === 4) { - let blob; - let filename = ''; - let type = xhr.getResponseHeader('Content-Type'); - let disposition = xhr.getResponseHeader('Content-Disposition'); - let url = window.URL || window.webkitURL; - - if (disposition && disposition.indexOf('attachment') !== -1) { - let filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; - let matches = filenameRegex.exec(disposition); - - if (matches !== null && matches[1]) { - filename = matches[1].replace(/['"]/g, ''); - } - } - - if (typeof File === 'function') { - try { - blob = new File([this.response], filename, { type: type }); - } catch (e) { - /* IE 10 or less do not support constructor for File. - In this case we will use Blob on next step. */ - } - } - - if (this.status === 404) { - try { - const jsonBody = JSON.parse(new TextDecoder().decode(this.response)); - action.failed(jsonBody); - } catch (e) { - action.failed({}); - } - return; - } - - if (typeof blob === 'undefined') { - blob = new Blob([this.response], { type: type }); - } - - action.completed(); - - if (typeof window.navigator.msSaveBlob !== 'undefined') { - window.navigator.msSaveBlob(blob, filename); - - return; - } - - let downloadUrl = url.createObjectURL(blob); - - if (filename) { - // use HTML5 a[download] attribute to specify filename. - let a = document.createElement('a'); - // safari doesn't support this yet - if (typeof a.download === 'undefined') { - window.location = downloadUrl; - } else { - a.href = downloadUrl; - a.download = filename; - document.body.appendChild(a); - a.click(); - } - - return; - } - - window.location = downloadUrl; - } - }); - - xhr.send(); -}; - - -Actions.downloadReportJsonFiles.listen(function (token, reportId) { - let url = settings.apiServer + - '/rpc/checkup_report_json_download?checkup_report_id=' + reportId; - Actions.downloadBinaryFile(this, token, url); -}); - -Actions.deleteOrgDomain.listen(function (token, orgId, domainId) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed({ orgId, domainId }); - timeoutPromise(REQUEST_TIMEOUT, api.deleteOrgDomain(token, domainId)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed({ orgId, domainId }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.addOrgDomain.listen(function (token, orgId, domain) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed({ orgId, domain }); - timeoutPromise(REQUEST_TIMEOUT, api.addOrgDomain(token, orgId, domain)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed({ orgId, domain }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.getJoeInstances.listen(function (token, orgId, projectId) { - this.progressed(); - - actionResult.bind(this)( - api.getJoeInstances(token, orgId, projectId), - (json) => { - this.completed(orgId, projectId, json); - }); -}); - -Actions.getJoeInstanceChannels.listen(function (token, instanceId) { - this.progressed(instanceId); - actionResult.bind(this)( - api.getJoeInstanceChannels(token, instanceId), - (json) => { - this.completed(instanceId, json); - }, - (err) => { - this.failed(instanceId, err); - }); -}); - -Actions.sendJoeInstanceCommand.listen(function (token, instanceId, - channelId, command, sessionId) { - this.progressed(instanceId); - actionResult.bind(this)( - api.sendJoeInstanceCommand(token, instanceId, channelId, command, sessionId), - (json) => { - json.message = command; - this.completed(instanceId, json); - }, - (err) => { - this.failed(instanceId, err); - } - ); -}); - -Actions.getJoeInstanceMessages.listen(function (token, instanceId, - channelId, sessionId) { - this.progressed(instanceId); - actionResult.bind(this)( - api.getJoeInstanceMessages(token, channelId, sessionId), - (json) => { - this.completed(instanceId, channelId, json); - }, - (err) => { - this.failed(instanceId, channelId, err); - }); -}); - -Actions.getJoeMessageArtifacts.listen(function (token, instanceId, channelId, messageId) { - this.progressed(instanceId, channelId, messageId); - actionResult.bind(this)( - api.getJoeMessageArtifacts(token, messageId), - (json) => { - this.completed(instanceId, channelId, messageId, json); - }, - (err) => { - this.failed(instanceId, channelId, messageId, err); - } - ); -}); - -Actions.checkJoeInstanceUrl.listen(function (token, instanceData) { - this.progressed(); - - instanceData.dryRun = true; - - actionResult.bind(this)(api.addJoeInstance(token, instanceData)); -}); - -Actions.addJoeInstance.listen(function (token, instanceData) { - this.progressed(); - - instanceData.dryRun = false; - - actionResult.bind(this)( - api.addJoeInstance(token, instanceData), - (json) => { - this.completed(instanceData.orgId, json); - } - ); -}); - -Actions.destroyJoeInstance.listen(function (token, instanceId) { - this.progressed(instanceId); - - actionResult.bind(this)( - api.destroyJoeInstance(token, instanceId), - (json) => { - this.completed(instanceId, json); - } - ); -}); - -Actions.getExternalVisualizationData.listen(function (type, plan, query) { - if (type !== visualizeTypes.depesz && type !== visualizeTypes.pev2) { - console.log('Unsupported visualization type.'); - return; - } - - this.progressed(type, plan, query); - - if (type === visualizeTypes.pev2) { - explainPev2Api.postPlan(plan.json, query) - .then((result) => { - result.json() - .then((response) => { - console.log(response.json); - - if (!response || !response.id) { - this.failed(type); - return; - } - - const url = `${settings.explainPev2Server}#${response.id}`; - this.completed(type, plan, query, url); - }).catch((error) => { - console.error('Error:', error); - this.failed(type); - }); - }).catch((error) => { - console.error('Error:', error); - this.failed(type); - }); - } else { - explainDepeszApi.postPlan(plan.text, query) - .then((response) => { - console.log(response); - - const xFinalUrl = response.headers.get('x-final-url'); - - let url = response.url; - if (response.url === settings.explainDepeszServer && !!xFinalUrl) { - url = xFinalUrl; - } - - this.completed(type, plan, query, url); - }).catch((error) => { - console.error('Error:', error); - this.failed(type); - }); - } -}); - -Actions.deleteJoeSessions.listen(function (token, ids) { - this.progressed(ids); - - actionResult.bind(this)( - api.deleteJoeSessions(token, ids), - (json) => { - this.completed(ids, json); - } - ); -}); - -Actions.deleteJoeCommands.listen(function (token, ids) { - this.progressed(ids); - - actionResult.bind(this)( - api.deleteJoeCommands(token, ids), - (json) => { - this.completed(ids, json); - } - ); -}); - - -Actions.deleteCheckupReports.listen(function (token, ids) { - this.progressed(ids); - - actionResult.bind(this)( - api.deleteCheckupReports(token, ids), - (json) => { - this.completed(ids, json); - } - ); -}); - -Actions.joeCommandFavorite.listen(function (token, commandId, favorite) { - this.progressed(commandId, favorite); - - actionResult.bind(this)( - api.joeCommandFavorite(token, commandId, favorite), - (json) => { - this.completed(json, commandId, favorite); - } - ); -}); - -Actions.getSharedUrlData.listen(function (uuid) { - this.progressed(uuid); - - actionResult.bind(this)( - api.getSharedUrlData(uuid), - (json) => { - this.completed(uuid, json); - } - ); -}); - -Actions.getSharedUrl.listen(function (token, orgId, objectType, objectId) { - this.progressed(orgId, objectType, objectId); - - actionResult.bind(this)( - api.getSharedUrl(token, orgId, objectType, objectId), - (json) => { - this.completed(orgId, objectType, objectId, json); - } - ); -}); - -Actions.removeSharedUrl.listen(function (token, orgId, objectType, objectId, urlId) { - this.progressed(); - - actionResult.bind(this)( - api.removeSharedUrl(token, orgId, urlId), - (json) => { - this.completed(orgId, objectType, objectId, urlId, json); - } - ); -}); - -Actions.addSharedUrl.listen(function (token, params) { - this.progressed(params); - - actionResult.bind(this)( - api.addSharedUrl(token, params), - (json) => { - this.completed(params, json); - } - ); -}); - -Actions.getBillingDataUsage.listen(function (token, orgId) { - this.progressed(orgId); - - actionResult.bind(this)( - api.getBillingDataUsage(token, orgId), - (json) => { - this.completed(orgId, json); - } - ); -}); - -Actions.subscribeBilling.listen(function (token, orgId, paymentMethodId) { - this.progressed(orgId, paymentMethodId); - - actionResult.bind(this)( - api.subscribeBilling(token, orgId, paymentMethodId), - (json) => { - this.completed(orgId, paymentMethodId, json); - } - ); -}); - -Actions.getDbLabInstance.listen(function (token, orgId, projectId, instanceId) { - this.progressed(orgId, projectId, instanceId); - - actionResult.bind(this)( - api.getDbLabInstances(token, orgId, projectId, instanceId), - (json) => { - this.completed(orgId, projectId, instanceId, json); - }, - (err) => { - this.failed(orgId, projectId, instanceId, err); - } - ); -}); - -Actions.getJoeInstance.listen(function (token, orgId, projectId, instanceId) { - this.progressed(orgId, projectId, instanceId); - - actionResult.bind(this)( - api.getJoeInstances(token, orgId, projectId, instanceId), - (json) => { - this.completed(orgId, projectId, instanceId, json); - }, - (err) => { - this.failed(orgId, projectId, instanceId, err); - } - ); -}); - -Actions.getDbLabSessions.listen(function (token, params) { - this.progressed(params); - - actionResult.bind(this)( - api.getDbLabSessions(token, params), - (json, count) => { - this.completed(params, json, count); - }, - (err) => { - this.failed(params, err); - } - ); -}); - -Actions.getDbLabSession.listen(function (token, sessionId) { - this.progressed(sessionId); - - actionResult.bind(this)( - api.getDbLabSession(token, sessionId), - (json) => { - this.completed(sessionId, json); - }, - (err) => { - this.failed(sessionId, err); - } - ); -}); - -Actions.getDbLabSessionLogs.listen(function (token, params) { - this.progressed(params); - - actionResult.bind(this)( - api.getDbLabSessionLogs(token, params), - (json, count) => { - this.completed(params, json, count); - }, - (err) => { - this.failed(params, err); - } - ); -}); - -Actions.getDbLabSessionArtifacts.listen(function (token, sessionId) { - this.progressed(sessionId); - - actionResult.bind(this)( - api.getDbLabSessionArtifacts(token, sessionId), - (json) => { - this.completed(sessionId, json); - }, - (err) => { - this.failed(sessionId, err); - } - ); -}); - -Actions.getDbLabSessionArtifact.listen(function (token, sessionId, artifactType) { - this.progressed(sessionId, artifactType); - - actionResult.bind(this)( - api.getDbLabSessionArtifact(token, sessionId, artifactType), - (json) => { - this.completed(sessionId, artifactType, json); - }, - (err) => { - this.failed(sessionId, artifactType, err); - } - ); -}); - - -Actions.updateOrgUser.listen(function (token, orgId, userId, role) { - this.progressed(orgId, userId, role); - - actionResult.bind(this)( - api.updateOrgUser(token, orgId, userId, role), - (json) => { - this.completed(orgId, userId, role, json); - }, - (err) => { - this.failed(orgId, userId, role, err); - } - ); -}); - -Actions.deleteOrgUser.listen(function (token, orgId, userId) { - this.progressed(orgId, userId); - - actionResult.bind(this)( - api.deleteOrgUser(token, orgId, userId), - (json) => { - this.completed(orgId, userId, json); - }, - (err) => { - this.failed(orgId, userId, err); - } - ); -}); - -Actions.getAuditLog.listen(function (token, params) { - this.progressed(params); - - actionResult.bind(this)( - api.getAuditLog(token, params), - (json, count) => { - this.completed(params, json, count); - }, - (err) => { - this.failed(params, err); - } - ); -}); - -Actions.downloadDblabSessionLog.listen(function (token, sessionId) { - let url = settings.apiServer + - '/rpc/dblab_session_logs_download?dblab_session_id=' + sessionId; - Actions.downloadBinaryFile(this, token, url); -}); - - -Actions.downloadDblabSessionArtifact.listen(function (token, sessionId, artifactType) { - let url = settings.apiServer + - '/rpc/dblab_session_artifacts_download?' + - 'dblab_session_id=' + sessionId + - '&artifact_type=' + artifactType; - Actions.downloadBinaryFile(this, token, url, { artifactType }); -}); - -Actions.sendUserCode.listen(function (token) { - this.progressed(); - - actionResult.bind(this)( - api.sendUserCode(token), - (json, count) => { - this.completed(json, count); - }, - (err) => { - this.failed(err); - } - ); -}); - -Actions.confirmUserEmail.listen(function (token, code) { - this.progressed(); - - actionResult.bind(this)( - api.confirmUserEmail(token, code), - (json) => { - this.completed(json); - }, - (err) => { - this.failed(err); - } - ); -}); - -Actions.confirmTosAgreement.listen(function (token) { - this.progressed(); - - actionResult.bind(this)( - api.confirmTosAgreement(token), - (json) => { - this.completed(json); - }, - (err) => { - this.failed(err); - } - ); -}); - - -Actions.testSiemServiceConnection.listen(function (token, data) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed(data); - timeoutPromise(REQUEST_TIMEOUT, api.testSiemServiceConnection(token, data)) - - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed(json); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -Actions.getAuditEvents.listen(function (token) { - let action = this; - - if (!api) { - settings.init(function () { - api = new Api(settings); - }); - } - - action.progressed(); - - timeoutPromise(REQUEST_TIMEOUT, api.getAuditEvents(token)) - .then(result => { - result.json() - .then(json => { - if (json) { - action.completed({ data: json }); - } else { - action.failed(new Error('wrong_reply')); - } - }) - .catch(err => { - console.error(err); - action.failed(new Error('wrong_reply')); - }); - }) - .catch(err => { - console.error(err); - if (err && err.message && err.message === 'timeout') { - action.failed(new Error('failed_fetch')); - } else { - action.failed(new Error('wrong_reply')); - } - }); -}); - -export default Actions; diff --git a/ui/packages/platform/src/api/api.js b/ui/packages/platform/src/api/api.js deleted file mode 100644 index ac0f0b56..00000000 --- a/ui/packages/platform/src/api/api.js +++ /dev/null @@ -1,1127 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import 'es6-promise/auto'; -import 'whatwg-fetch'; - -function encodeData(data) { - return Object.keys(data).map(function (key) { - return [key, data[key]].map(encodeURIComponent).join('='); - }).join('&'); -} - -class Api { - constructor(setting) { - this.server = setting.server - this.apiServer = process.env.REACT_APP_API_URL_PREFIX || setting.apiServer // if set in .env (e.g., for dev/debug), use it - } - - get(url, query, options) { - let params = ''; - - if (query) { - params = `?${encodeData(query)}`; - } - - if (options) { - options.Prefer = 'count=none'; - } - - let fetchOptions = { - ...options, - method: 'get', - credentials: 'include' - }; - - return fetch(`${url}${params}`, fetchOptions); - } - - post(url, data, options = {}) { - let headers = options.headers || {}; - - let fetchOptions = { - ...options, - method: 'post', - credentials: 'include', - headers: { - ...headers, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }; - - return fetch(url, fetchOptions); - } - - patch(url, data, options = {}) { - let headers = options.headers || {}; - - let fetchOptions = { - ...options, - method: 'PATCH', - credentials: 'include', - headers: { - ...headers, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }; - - return fetch(url, fetchOptions); - } - - delete(url, data, options = {}) { - let headers = options.headers || {}; - - let fetchOptions = { - ...options, - method: 'DELETE', - credentials: 'include', - headers: { - ...headers, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }; - - return fetch(url, fetchOptions); - } - - login(login, password) { - let headers = {}; - - return this.post(`${this.apiServer}/rpc/login`, { - email: login, - password: password - }, { - headers: headers - }); - } - - getUserProfile(token) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.get(`${this.apiServer}/user_get`, {}, { - headers: headers - }); - } - - updateUserProfile(token, data) { - let headers = { - Authorization: 'Bearer ' + token, - Accept: 'application/vnd.pgrst.object+json' - }; - - let body = {}; - - if (data.is_chats_email_notifications_enabled !== 'undefined') { - body.chats_email_notifications_enabled = data.is_chats_email_notifications_enabled; - } - - if (data.first_name !== 'undefined') { - body.first_name = data.first_name; - } - - if (data.last_name !== 'undefined') { - body.last_name = data.last_name; - } - - if (data.dblab_low_disk_space_notifications_enabled !== 'undefined') { - body.dblab_low_disk_space_notifications_enabled = data.dblab_low_disk_space_notifications_enabled; - } - - if (data.dblab_old_clones_notifications_enabled !== 'undefined') { - body.dblab_old_clones_notifications_enabled = data.dblab_old_clones_notifications_enabled; - } - - return this.post(`${this.apiServer}/rpc/update_user_profile`, body, { - headers: headers - }); - } - - getAccessTokens(token, orgId) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token - }; - - if (orgId !== null && orgId !== 0) { - params.org_id = `eq.${orgId}`; - } - - return this.get(`${this.apiServer}/api_tokens`, params, { - headers: headers - }); - } - - getAccessToken(token, name, expires, orgId, isPersonal) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/api_token_create`, { - name: name, - expires: expires, - org_id: orgId, - is_personal: isPersonal - }, { - headers: headers - }); - } - - revokeAccessToken(token, id) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/api_token_revoke`, { id: id }, { - headers: headers - }); - } - - getCheckupReports(token, orgId, projectId, reportId) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token - }; - - if (orgId !== null && orgId !== 0) { - params.org_id = `eq.${orgId}`; - } - - if (projectId !== null && projectId !== 0) { - params.project_id = `eq.${projectId}`; - } - - if (typeof reportId !== 'undefined' && reportId !== 0) { - params.id = `eq.${reportId}`; - } - - return this.get(`${this.apiServer}/checkup_reports`, params, { - headers: headers - }); - } - - getCheckupReportFiles(token, reportId, type, orderBy, orderDirection) { - let headers = { - Authorization: 'Bearer ' + token - }; - let params = { - checkup_report_id: `eq.${reportId}` - }; - - if (type) { - params.type = `eq.${type}`; - } - - if (orderBy && orderDirection) { - params.order = `${orderBy}.${orderDirection}`; - } - - return this.get(`${this.apiServer}/checkup_report_files`, params, { - headers: headers - }); - } - - getCheckupReportFile(token, projectId, reportId, fileId, type) { - let headers = { - Authorization: 'Bearer ' + token - }; - let params = { - project_id: `eq.${projectId}`, - checkup_report_id: `eq.${reportId}`, - type: `eq.${type}` - }; - - if (fileId === parseInt(fileId, 10)) { - params.id = `eq.${fileId}`; - } else { - params.filename = `eq.${fileId}`; - } - - return this.get(`${this.apiServer}/checkup_report_file_data`, params, { - headers: headers - }); - } - - getProjects(token, orgId) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token - }; - - if (orgId) { - params.org_id = `eq.${orgId}`; - } - - return this.get(`${this.apiServer}/projects`, params, { - headers: headers - }); - } - - getJoeSessions(token, orgId, projectId) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token - }; - - if (orgId !== null && orgId !== 0) { - params.org_id = `eq.${orgId}`; - } - - if (projectId !== null && projectId !== 0) { - params.project_id = `eq.${projectId}`; - } - - return this.get(`${this.apiServer}/joe_sessions`, params, { - headers: headers - }); - } - - getJoeSessionCommands(token, - { orgId, session, fingerprint, command, project, - author, startAt, limit, lastId, search, isFavorite }) { - const params = {}; - const headers = { - Authorization: 'Bearer ' + token - }; - - if (orgId && orgId !== 0) { - params['org_id'] = `eq.${orgId}`; - } - - if (session && session !== 0) { - params['joe_session_id'] = `eq.${session}`; - } - - if (fingerprint) { - params.fingerprint = `ilike.${fingerprint}*`; - } - - if (command) { - params.command = `eq.${command}`; - } - - if (startAt) { - params.created_at = `gt.${startAt}`; - } - - if (limit) { - params.limit = limit; - } - - if (lastId) { - // backend order by id.desc - params.id = `lt.${lastId}`; - } - - if (project) { - // backend order by id.desc - params.project_name = `ilike.${project}*`; - } - - if (author) { - params.or = `(username.ilike.${author}*,` + - `useremail.ilike.${author}*,` + - `slack_username.ilike.${author}*)`; - } - - if (search) { - let searchText = encodeURIComponent(search); - params.tsv = `fts(simple).${searchText}`; - } - - if (isFavorite) { - params.is_favorite = `gt.0`; - } - - - return this.get(`${this.apiServer}/joe_session_commands`, params, { - headers: headers - }); - } - - getJoeSessionCommand(token, orgId, commandId) { - let params = { org_id: `eq.${orgId}` }; - let headers = { - Authorization: 'Bearer ' + token - }; - - if (!!commandId && commandId !== 0) { - params.id = `eq.${commandId}`; - } - - return this.get(`${this.apiServer}/joe_session_commands`, params, { - headers: headers - }); - } - - getOrgs(token, orgId) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token - }; - - if (orgId) { - params.id = `eq.${orgId}`; - } - - return this.get(`${this.apiServer}/orgs`, params, { - headers: headers - }); - } - - getOrgUsers(token, orgId) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token - }; - - if (orgId) { - params.id = `eq.${orgId}`; - } - - return this.get(`${this.apiServer}/org_users`, params, { - headers: headers - }); - } - - updateOrg(token, orgId, orgData) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token, - prefer: 'return=representation' - }; - - if (orgData.name) { - params.name = orgData.name; - } - - if (orgData.alias) { - params.alias = orgData.alias; - } - - if (typeof orgData.users_autojoin !== 'undefined') { - params.users_autojoin = orgData.users_autojoin; - } - - if (typeof orgData.onboarding_text !== 'undefined') { - params.onboarding_text = orgData.onboarding_text; - } - - if (typeof orgData.oauth_allow_google !== 'undefined') { - params.oauth_allow_google = orgData.oauth_allow_google; - } - - if (typeof orgData.oauth_allow_linkedin !== 'undefined') { - params.oauth_allow_linkedin = orgData.oauth_allow_linkedin; - } - - if (typeof orgData.oauth_allow_github !== 'undefined') { - params.oauth_allow_github = orgData.oauth_allow_github; - } - - if (typeof orgData.oauth_allow_gitlab !== 'undefined') { - params.oauth_allow_gitlab = orgData.oauth_allow_gitlab; - } - - return this.patch(`${this.apiServer}/orgs?id=eq.` + orgId, params, { - headers: headers - }); - } - - createOrg(token, orgData) { - let params = { - name: orgData.name, - alias: orgData.alias - }; - let headers = { - Authorization: 'Bearer ' + token - }; - - if (orgData.email_domain_autojoin) { - params.org_domain = orgData.email_domain_autojoin; - } - - if (typeof orgData.users_autojoin !== 'undefined') { - params.users_autojoin = orgData.users_autojoin; - } - - return this.post(`${this.apiServer}/rpc/user_create_org`, params, { - headers: headers - }); - } - - addOrgDomain(token, orgId, domain) { - let params = { - org_id: orgId, - domain_name: domain - }; - let headers = { - Authorization: 'Bearer ' + token, - prefer: 'return=representation' - }; - - return this.post(`${this.apiServer}/org_domains`, params, { - headers: headers - }); - } - - deleteOrgDomain(token, domainId) { - let headers = { - Authorization: 'Bearer ' + token, - prefer: 'return=representation' - }; - - return this.delete(`${this.apiServer}/org_domains?id=eq.${domainId}`, {}, { - headers: headers - }); - } - - updateAiBotSettings(token, orgId, orgData) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token, - prefer: 'return=representation' - }; - - if (typeof orgData.is_chat_public_by_default !== 'undefined') { - params.is_chat_public_by_default = orgData.is_chat_public_by_default; - } - - return this.patch(`${this.apiServer}/orgs?id=eq.` + orgId, params, { - headers: headers - }); - } - - updateAuditSettings(token, orgId, orgData) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token, - prefer: 'return=representation' - }; - - if (typeof orgData.enableSiemIntegration !== 'undefined') { - params.siem_integration_enabled = orgData.enableSiemIntegration; - } - - if (typeof orgData.urlSchema !== 'undefined') { - params.siem_integration_url = orgData.urlSchema; - } - - if (typeof orgData.auditEvents !== "undefined") { - params.audit_events_to_log = orgData.auditEvents.map((item) => item.event_name) - } - - if (typeof orgData.headers !== 'undefined' && Array.isArray(orgData.headers)) { - orgData.headers = orgData.headers.filter(item => item.key && item.value); - if (Object.keys(orgData.headers).length > 0) { - params.siem_integration_request_headers = orgData.headers.reduce((acc, item) => { - acc[item.key] = item.value; - return acc; - }, {}); - } else { - params.siem_integration_request_headers = null - } - } - - return this.patch(`${this.apiServer}/orgs?id=eq.` + orgId, params, { - headers: headers - }); - } - - updateDBLabSettings(token, orgId, orgData) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token, - prefer: 'return=representation' - }; - - if (typeof orgData.dblab_low_disk_space_notifications_threshold_percent !== 'undefined') { - params.dblab_low_disk_space_notifications_threshold_percent = orgData.dblab_low_disk_space_notifications_threshold_percent - } - - if (typeof orgData.dblab_old_clones_notifications_threshold_hours !== 'undefined') { - params.dblab_old_clones_notifications_threshold_hours = orgData.dblab_old_clones_notifications_threshold_hours - } - - return this.patch(`${this.apiServer}/orgs?id=eq.` + orgId, params, { - headers: headers - }); - } - - - testSiemServiceConnection(token, data) { - let params = {}; - let headers = { - Accept: 'application/vnd.pgrst.object+json', - Authorization: 'Bearer ' + token, - prefer: 'return=representation' - }; - - if (typeof data.urlSchema !== 'undefined') { - params.api_url = data.urlSchema; - } - - if (typeof data.headers !== 'undefined' && Array.isArray(data.headers)) { - data.headers = data.headers.filter(item => item.key && item.value); - if (Object.keys(data.headers).length > 0) { - params.http_headers_extra = data.headers.reduce((acc, item) => { - acc[item.key] = item.value; - return acc; - }, {}); - } else { - params.http_headers_extra = null - } - } - - return this.post(`${this.apiServer}/rpc/test_siem_connection`, params, { - headers: headers - }); - } - - inviteUser(token, orgId, email) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/user_invite_org_user`, { - org_id: orgId, - email: email - }, { - headers: headers - }); - } - - useDemoData(token) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/use_demo_data`, {}, { - headers: headers - }); - } - - getDbLabInstances(token, orgId, projectId, instanceId) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token - }; - - if (orgId !== null && orgId !== 0) { - params.org_id = `eq.${orgId}`; - } - if (projectId !== null && projectId !== 0) { - params.project_id = `eq.${projectId}`; - } - if (typeof instanceId !== 'undefined' && instanceId !== 0) { - params.id = `eq.${instanceId}`; - } - - return this.get(`${this.apiServer}/dblab_instances`, params, { - headers: headers - }); - } - - addDbLabInstance(token, instanceData) { - let headers = { - Authorization: 'Bearer ' + token - }; - let params = { - url: instanceData.url, - org_id: instanceData.orgId, - token: instanceData.instanceToken, - project: instanceData.project, - project_label: instanceData.projectLabel, - use_tunnel: instanceData.useTunnel, - ssh_server_url: instanceData.sshServerUrl - }; - - return this.post(`${this.apiServer}/rpc/dblab_instance_create`, params, { - headers: headers - }); - } - - editDbLabInstance(token, instanceData) { - let headers = { - Authorization: 'Bearer ' + token, - } - let params = { - url: instanceData.url, - instance_id: Number(instanceData.instanceId), - project_name: instanceData.project, - project_label: instanceData.projectLabel, - use_tunnel: instanceData.useTunnel, - ssh_server_url: instanceData.sshServerUrl, - } - - return this.post(`${this.apiServer}/rpc/dblab_instance_edit`, params, { - headers: headers, - }) - } - - destroyDbLabInstance(token, instanceId) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/dblab_instance_destroy`, { - instance_id: instanceId - }, { - headers: headers - }); - } - - getDbLabInstanceStatus(token, instanceId) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/dblab_instance_status_refresh`, { - instance_id: instanceId - }, { - headers: headers - }); - } - - checkDbLabInstanceUrl(token, url, verifyToken, useTunnel) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/dblab_instance_status`, { - url: url, - verify_token: verifyToken, - use_tunnel: useTunnel - }, { - headers: headers - }); - } - - getJoeInstances(token, orgId, projectId, instanceId) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token - }; - - if (orgId !== null && orgId !== 0) { - params.org_id = `eq.${orgId}`; - } - if (typeof projectId !== 'undefined' && projectId !== 0) { - params.project_id = `eq.${projectId}`; - } - if (typeof instanceId !== 'undefined' && instanceId !== 0) { - params.id = `eq.${instanceId}`; - } - - return this.get(`${this.apiServer}/joe_instances`, params, { - headers: headers - }); - } - - getJoeInstanceChannels(token, instanceId) { - let params = { - instance_id: instanceId - }; - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.get(`${this.apiServer}/rpc/joe_instance_channels_get`, params, { - headers: headers - }); - } - - sendJoeInstanceCommand(token, instanceId, channelId, command, sessionId) { - let params = { - instance_id: instanceId, - channel_id: channelId, - command: command - }; - let headers = { - Authorization: 'Bearer ' + token - }; - - if (sessionId !== null && sessionId !== 0) { - params.session_id = sessionId; - } - - return this.post(`${this.apiServer}/rpc/joe_command_send`, params, { - headers: headers - }); - } - - getJoeInstanceMessages(token, channelId, sessionId) { - let params = { - channel_id: `eq.${channelId}`, - session_id: `eq.${sessionId}` - }; - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.get(`${this.apiServer}/joe_messages`, params, { - headers: headers - }); - } - - getJoeMessageArtifacts(token, messageId) { - let params = { - message_id: `eq.${messageId}` - }; - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.get(`${this.apiServer}/joe_message_artifacts`, params, { - headers: headers - }); - } - - addJoeInstance(token, instanceData) { - let headers = { - Authorization: 'Bearer ' + token - }; - let params = { - url: instanceData.url, - org_id: instanceData.orgId, - token: instanceData.verifyToken, - project: instanceData.project, - use_tunnel: instanceData.useTunnel, - dry_run: instanceData.dryRun - }; - - if (instanceData.useTunnel && instanceData.sshServerUrl) { - params.ssh_server_url = instanceData.sshServerUrl; - } - - return this.post(`${this.apiServer}/rpc/joe_instance_create`, params, { - headers: headers - }); - } - - destroyJoeInstance(token, instanceId) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/joe_instance_destroy`, { - instance_id: instanceId - }, { - headers: headers - }); - } - - deleteJoeSessions(token, ids) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/joe_session_delete`, { - ids: ids - }, { - headers: headers - }); - } - - deleteJoeCommands(token, ids) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/joe_command_delete`, { ids }, - { headers }); - } - - - deleteCheckupReports(token, ids) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/checkup_report_delete`, { - ids: ids - }, { - headers: headers - }); - } - - joeCommandFavorite(token, commandId, favorite) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/joe_command_favorite`, { - command_id: parseInt(commandId, 10), - favorite - }, { headers }); - } - - getSharedUrlData(uuid) { - return this.get(`${this.apiServer}/rpc/shared_url_get_data`, { uuid }, {}); - } - - getSharedUrl(token, org_id, object_type, object_id) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/shared_url_get`, { - org_id, - object_type, - object_id - }, { headers }); - } - - addSharedUrl(token, urlParams) { - let headers = { - Authorization: 'Bearer ' + token - }; - let params = { - org_id: urlParams.orgId, - url: urlParams.url, - object_type: urlParams.objectType, - object_id: urlParams.objectId - }; - - if (urlParams.uuid) { - params['uuid'] = urlParams.uuid; - } - - return this.post(`${this.apiServer}/rpc/shared_url_add`, params, { headers }); - } - - removeSharedUrl(token, org_id, id) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post(`${this.apiServer}/rpc/shared_url_remove`, { org_id, id }, { headers }); - } - - subscribeBilling(token, org_id, payment_method_id) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post( - `${this.apiServer}/rpc/billing_subscribe`, - { org_id, payment_method_id }, - { headers } - ); - } - - getBillingDataUsage(token, orgId) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.get( - `${this.apiServer}/billing_data_usage`, - { org_id: `eq.${orgId}` }, - { headers } - ); - } - - getDbLabSessions(token, { orgId, projectId, instanceId, limit, lastId }) { - let headers = { - Authorization: 'Bearer ' + token, - Prefer: 'count=exact' - }; - - let params = { - org_id: `eq.${orgId}` - }; - - if (typeof projectId !== 'undefined' && projectId) { - params.project_id = `eq.$(projectId)`; - } - - if (typeof instanceId !== 'undefined' && instanceId) { - params.instance_id = `eq.$(instanceId)`; - } - - if (lastId) { - params.id = `lt.${lastId}`; - } - - if (limit) { - params.limit = limit; - } - - return this.get(`${this.apiServer}/dblab_sessions`, params, { - headers: headers - }); - } - - getDbLabSession(token, sessionId) { - let headers = { - Authorization: 'Bearer ' + token - }; - - let params = { - id: `eq.${sessionId}` - }; - - return this.get(`${this.apiServer}/dblab_sessions`, params, { - headers: headers - }); - } - - getDbLabSessionLogs(token, { sessionId, limit, lastId }) { - let headers = { - Authorization: 'Bearer ' + token, - Prefer: 'count=exact' - }; - - let params = { - dblab_session_id: `eq.${sessionId}` - }; - - if (lastId) { - params.id = `lt.${lastId}`; - } - - if (limit) { - params.limit = limit; - } - - return this.get(`${this.apiServer}/dblab_session_logs`, params, { - headers: headers - }); - } - - getDbLabSessionArtifacts(token, sessionId) { - let headers = { - Authorization: 'Bearer ' + token, - Prefer: 'count=exact' - }; - - let params = { - dblab_session_id: `eq.${sessionId}` - }; - - return this.get(`${this.apiServer}/dblab_session_artifacts`, params, { - headers: headers - }); - } - - getDbLabSessionArtifact(token, sessionId, artifactType) { - let headers = { - Authorization: 'Bearer ' + token, - Prefer: 'count=exact' - }; - - let params = { - dblab_session_id: `eq.${sessionId}`, - artifact_type: `eq.${artifactType}` - }; - - return this.get(`${this.apiServer}/dblab_session_artifacts_data`, params, { - headers: headers - }); - } - - updateOrgUser(token, org_id, user_id, role_id) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post( - `${this.apiServer}/rpc/user_update_org_user`, - { org_id, user_id, role_id }, - { headers } - ); - } - - deleteOrgUser(token, org_id, user_id) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post( - `${this.apiServer}/rpc/user_delete_org_user`, - { org_id, user_id }, - { headers } - ); - } - - getAuditLog(token, { orgId, lastId, limit }) { - let headers = { - Authorization: 'Bearer ' + token, - Prefer: 'count=exact' - }; - - let params = { - org_id: `eq.${orgId}` - }; - - if (lastId) { - params.id = `lt.${lastId}`; - } - - if (limit) { - params.limit = limit; - } - - return this.get(`${this.apiServer}/audit_log`, params, { - headers: headers - }); - } - - sendUserCode(token) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post( - `${this.apiServer}/rpc/user_send_code`, - {}, - { headers } - ); - } - - confirmUserEmail(token, verification_code) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post( - `${this.apiServer}/rpc/user_confirm_email`, - { verification_code }, - { headers } - ); - } - - confirmTosAgreement(token) { - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.post( - `${this.apiServer}/rpc/user_confirm_tos_agreement`, - {}, - { headers } - ); - } - - getAuditEvents(token) { - let params = {}; - let headers = { - Authorization: 'Bearer ' + token - }; - - return this.get(`${this.apiServer}/audit_events`, params, { - headers: headers - }); - } -} - -export default Api; diff --git a/ui/packages/platform/src/api/billing/getPaymentMethods.ts b/ui/packages/platform/src/api/billing/getPaymentMethods.ts deleted file mode 100644 index 51191867..00000000 --- a/ui/packages/platform/src/api/billing/getPaymentMethods.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { request } from 'helpers/request' - -export const getPaymentMethods = async (orgId: number) => { - const response = await request(`/rpc/billing_payment_methods`, { - headers: { - Accept: 'application/vnd.pgrst.object+json', - }, - method: 'POST', - body: JSON.stringify({ - org_id: orgId, - }), - }) - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/billing/getSubscription.ts b/ui/packages/platform/src/api/billing/getSubscription.ts deleted file mode 100644 index 2dc4cbf8..00000000 --- a/ui/packages/platform/src/api/billing/getSubscription.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { request } from 'helpers/request' - -export const getSubscription = async (orgId: number) => { - const response = await request(`/rpc/billing_subscriptions`, { - headers: { - Accept: 'application/vnd.pgrst.object+json', - }, - method: 'POST', - body: JSON.stringify({ - org_id: orgId, - }), - }) - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/billing/startBillingSession.ts b/ui/packages/platform/src/api/billing/startBillingSession.ts deleted file mode 100644 index fcfcd81e..00000000 --- a/ui/packages/platform/src/api/billing/startBillingSession.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { request } from 'helpers/request' - -export const startBillingSession = async (orgId: number, returnUrl: string) => { - const response = await request(`/rpc/billing_portal_start_session`, { - headers: { - Accept: 'application/vnd.pgrst.object+json', - }, - method: 'POST', - body: JSON.stringify({ - org_id: orgId, - return_url: returnUrl, - }), - }) - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : await response.json(), - } -} diff --git a/ui/packages/platform/src/api/bot/convertThread.ts b/ui/packages/platform/src/api/bot/convertThread.ts deleted file mode 100644 index bd9d9b2a..00000000 --- a/ui/packages/platform/src/api/bot/convertThread.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {request} from "../../helpers/request"; - -export const convertThread = (thread_id: string): Promise<{ response: { final_thread_id: string, msg: string } | null; error: Response | null }> => { - const apiServer = process.env.REACT_APP_BOT_API_URL || ''; - - return request( - `/convert_thread`, - { - method: 'POST', - body: JSON.stringify({ thread_id }), - }, - apiServer - ) - .then(async (response) => { - if (!response.ok) { - return { response: null, error: response }; - } - const responseData = await response.json(); - return { response: responseData, error: null }; - }) - .catch((error: Response) => { - return { response: null, error }; - }); -}; \ No newline at end of file diff --git a/ui/packages/platform/src/api/bot/getAiModels.ts b/ui/packages/platform/src/api/bot/getAiModels.ts deleted file mode 100644 index eba0a0c0..00000000 --- a/ui/packages/platform/src/api/bot/getAiModels.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {request} from "../../helpers/request"; -import { AiModel } from "../../types/api/entities/bot"; - -export const getAiModels = async (orgId?: number): Promise<{ response: AiModel[] | null; error: Response | null }> => { - const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; - const body = { - org_id: orgId - } - try { - const response = await request(`${apiServer}/rpc/bot_llm_models`, { - method: 'POST', - body: JSON.stringify(body), - headers: { - 'Accept': 'application/vnd.pgrst.object+json', - 'Prefer': 'return=representation', - } - }); - - if (!response.ok) { - return { response: null, error: response }; - } - - const responseData: AiModel[] | null = await response.json(); - - return { response: responseData, error: null }; - - } catch (error) { - return { response: null, error: error as Response }; - } -} \ No newline at end of file diff --git a/ui/packages/platform/src/api/bot/getChats.ts b/ui/packages/platform/src/api/bot/getChats.ts deleted file mode 100644 index 1a47da61..00000000 --- a/ui/packages/platform/src/api/bot/getChats.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {request} from "../../helpers/request"; -import {BotMessage} from "../../types/api/entities/bot"; - -type Req = { - query?: string -} - -export const getChats = async (req: Req): Promise<{ response: BotMessage[] | null; error: Response | null }> => { - const { query } = req; - - const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; - - try { - const response = await request(`${apiServer}/chats_auth${query ? query : ''}`, { - method: 'GET', - }); - - if (!response.ok) { - return { response: null, error: response }; - } - - const responseData: BotMessage[] = await response.json(); - - return { response: responseData, error: null }; - - } catch (error) { - return { response: null, error: error as Response }; - } -} \ No newline at end of file diff --git a/ui/packages/platform/src/api/bot/getChatsWithWholeThreads.ts b/ui/packages/platform/src/api/bot/getChatsWithWholeThreads.ts deleted file mode 100644 index 2485eba8..00000000 --- a/ui/packages/platform/src/api/bot/getChatsWithWholeThreads.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {request} from "../../helpers/request"; -import {BotMessage} from "../../types/api/entities/bot"; - -type Req = { - id: string -} - -export const getChatsWithWholeThreads = async (req: Req): Promise<{ response: BotMessage[] | null; error: Response | null }> => { - const { id } = req; - - const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; - - try { - const response = await request(`${apiServer}/rpc/chats_ancestors_and_descendants`, { - method: 'POST', - body: JSON.stringify({ id }), - }); - - if (!response.ok) { - return { response: null, error: response }; - } - - const responseData: BotMessage[] = await response.json(); - - return { response: responseData, error: null }; - - } catch (error) { - return { response: null, error: error as Response }; - } -} \ No newline at end of file diff --git a/ui/packages/platform/src/api/bot/getDebugMessages.ts b/ui/packages/platform/src/api/bot/getDebugMessages.ts deleted file mode 100644 index 8d566312..00000000 --- a/ui/packages/platform/src/api/bot/getDebugMessages.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {request} from "../../helpers/request"; -import { DebugMessage } from "../../types/api/entities/bot"; - -type Req = - | { thread_id: string; message_id?: string } - | { thread_id?: string; message_id: string }; - -export const getDebugMessages = async (req: Req): Promise<{ response: DebugMessage[] | null; error: Response | null }> => { - const { thread_id, message_id } = req; - - const params: { [key: string]: string } = {}; - - if (thread_id) { - params['chat_thread_id'] = `eq.${thread_id}`; - } - - if (message_id) { - params['chat_msg_id'] = `eq.${message_id}`; - } - - const queryString = new URLSearchParams(params).toString(); - - const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; - - try { - const response = await request(`${apiServer}/chat_debug_messages?${queryString}`); - - if (!response.ok) { - return { response: null, error: response }; - } - - const responseData: DebugMessage[] = await response.json(); - - return { response: responseData, error: null }; - - } catch (error) { - return { response: null, error: error as Response }; - } -} \ No newline at end of file diff --git a/ui/packages/platform/src/api/bot/updateChatVisibility.ts b/ui/packages/platform/src/api/bot/updateChatVisibility.ts deleted file mode 100644 index 9a7dd486..00000000 --- a/ui/packages/platform/src/api/bot/updateChatVisibility.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {request} from "../../helpers/request"; -import {BotMessage} from "../../types/api/entities/bot"; - -type Req = { - thread_id: string, - is_public: boolean -} - -export const updateChatVisibility = async (req: Req): Promise<{ response: BotMessage | null; error: Response | null }> => { - const { thread_id, is_public } = req; - - const apiServer = process.env.REACT_APP_API_URL_PREFIX || ''; - - try { - const response = await request(`${apiServer}/chats_auth?thread_id=eq.${thread_id}`, { - method: 'PATCH', - headers: { - Prefer: 'return=representation' - }, - body: JSON.stringify({ - is_public, - }) - }); - - if (!response.ok) { - return { response: null, error: response }; - } - - const responseData: BotMessage = await response.json(); - - return { response: responseData, error: null }; - - } catch (error) { - return { response: null, error: error as Response }; - } -} diff --git a/ui/packages/platform/src/api/clones/createClone.ts b/ui/packages/platform/src/api/clones/createClone.ts deleted file mode 100644 index 6fbc7666..00000000 --- a/ui/packages/platform/src/api/clones/createClone.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { CloneDto, formatCloneDto } from '@postgres.ai/shared/types/api/entities/clone' - -import { request } from 'helpers/request' - -type Req = { - instanceId: string - cloneId: string - snapshotId: string - dbUser: string - dbPassword: string - isProtected: boolean -} - -export const createClone = async (req: Req) => { - const response = await request('/rpc/dblab_api_call', { - method: 'POST', - body: JSON.stringify({ - instance_id: req.instanceId, - action: '/clone', - method: 'post', - data: { - id: req.cloneId, - snapshot: { - id: req.snapshotId, - }, - db: { - username: req.dbUser, - password: req.dbPassword, - }, - protected: req.isProtected, - }, - }) - }) - - return { - response: response.ok ? formatCloneDto(await response.json() as CloneDto) : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/clones/destroyClone.ts b/ui/packages/platform/src/api/clones/destroyClone.ts deleted file mode 100644 index 40642639..00000000 --- a/ui/packages/platform/src/api/clones/destroyClone.ts +++ /dev/null @@ -1,26 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { DestroyClone } from '@postgres.ai/shared/types/api/endpoints/destroyClone' - -import { request } from 'helpers/request' - -export const destroyClone: DestroyClone = async (req) => { - const response = await request('/rpc/dblab_api_call', { - method: 'POST', - body: JSON.stringify({ - action: '/clone/' + encodeURIComponent(req.cloneId), - instance_id: req.instanceId, - method: 'delete' - }), - }) - - return { - response: response.ok ? true : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/clones/getClone.ts b/ui/packages/platform/src/api/clones/getClone.ts deleted file mode 100644 index 3534e426..00000000 --- a/ui/packages/platform/src/api/clones/getClone.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - CloneDto, - formatCloneDto, -} from '@postgres.ai/shared/types/api/entities/clone' - -import { request } from 'helpers/request' - -type Request = { - instanceId: string - cloneId: string -} - -export const getClone = async (req: Request) => { - const response = (await request('/rpc/dblab_api_call', { - method: 'POST', - body: JSON.stringify({ - action: '/clone/' + encodeURIComponent(req.cloneId), - instance_id: req.instanceId, - method: 'get' - }) - })) - - return { - response: response.ok - ? formatCloneDto(await response.json() as CloneDto) - : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/clones/resetClone.ts b/ui/packages/platform/src/api/clones/resetClone.ts deleted file mode 100644 index 0b09fe94..00000000 --- a/ui/packages/platform/src/api/clones/resetClone.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { ResetClone } from '@postgres.ai/shared/types/api/endpoints/resetClone' - -import { request } from 'helpers/request' - -export const resetClone: ResetClone = async (req) => { - const response = await request('/rpc/dblab_api_call', { - method: 'post', - body: JSON.stringify({ - action: '/clone/' + encodeURIComponent(req.cloneId) + '/reset', - instance_id: req.instanceId, - method: 'post', - data: { - snapshotID: req.snapshotId, - latest: false, - }, - }), - }) - - return { - response: response.ok ? true : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/clones/updateClone.ts b/ui/packages/platform/src/api/clones/updateClone.ts deleted file mode 100644 index a28b4870..00000000 --- a/ui/packages/platform/src/api/clones/updateClone.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { UpdateClone } from '@postgres.ai/shared/types/api/endpoints/updateClone' - -import { request } from 'helpers/request' - -export const updateClone: UpdateClone = async (req) => { - const response = await request('/rpc/dblab_api_call', { - method: 'POST', - body: JSON.stringify({ - action: '/clone/' + encodeURIComponent(req.cloneId), - instance_id: req.instanceId, - method: 'patch', - data: { - protected: req.clone.isProtected, - }, - }), - }) - - return { - response: response.ok ? true : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/cloud/getCloudImages.ts b/ui/packages/platform/src/api/cloud/getCloudImages.ts deleted file mode 100644 index c105cfc5..00000000 --- a/ui/packages/platform/src/api/cloud/getCloudImages.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { request } from 'helpers/request' - -export interface CloudImage { - api_name: string - os_name: string - os_version: string - arch: string - cloud_provider: string - region: string - native_os_image: string - release: string -} - -export interface CloudImagesRequest { - os_name: string - os_version: string - arch: string - cloud_provider: string - region: string -} - -export const getCloudImages = async (req: CloudImagesRequest) => { - const response = await request( - `/cloud_os_images?os_name=eq.${req.os_name}&os_version=eq.${req.os_version}&arch=eq.${req.arch}&cloud_provider=eq.${req.cloud_provider}®ion=eq.${req.region}`, - ) - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/cloud/getCloudInstances.ts b/ui/packages/platform/src/api/cloud/getCloudInstances.ts deleted file mode 100644 index 7467dbea..00000000 --- a/ui/packages/platform/src/api/cloud/getCloudInstances.ts +++ /dev/null @@ -1,41 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { request } from 'helpers/request' - -export interface CloudInstance { - api_name: string - arch: string - vcpus: number - ram_gib: number - dle_se_price_hourly: number - cloud_provider: string - only_in_regions: boolean | null - native_name: string - native_vcpus: number - native_ram_gib: number - native_reference_price_hourly: number - native_reference_price_currency: string - native_reference_price_region: string - native_reference_price_revision_date: string -} - -export interface CloudInstancesRequest { - provider: string - region: string -} - -export const getCloudInstances = async (req: CloudInstancesRequest) => { - const response = await request( - `/cloud_instances?cloud_provider=eq.${req.provider}&only_in_regions&only_in_regions=ov.{all,${req.region}}&order=vcpus.asc,ram_gib.asc`, - ) - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/cloud/getCloudProviders.ts b/ui/packages/platform/src/api/cloud/getCloudProviders.ts deleted file mode 100644 index a46983dd..00000000 --- a/ui/packages/platform/src/api/cloud/getCloudProviders.ts +++ /dev/null @@ -1,22 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { request } from 'helpers/request' - -export interface CloudProvider { - api_name: string - label: string -} - -export const getCloudProviders = async () => { - const response = await request('/cloud_providers') - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/cloud/getCloudRegions.ts b/ui/packages/platform/src/api/cloud/getCloudRegions.ts deleted file mode 100644 index 80b0ccfc..00000000 --- a/ui/packages/platform/src/api/cloud/getCloudRegions.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { request } from 'helpers/request' - -export interface CloudRegion { - api_name: string - cloud_provider: string - label: string - native_code: string - world_part: string -} - -export const getCloudRegions = async (req: string) => { - const response = await request(`/cloud_regions?cloud_provider=eq.${req}`) - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/cloud/getCloudVolumes.ts b/ui/packages/platform/src/api/cloud/getCloudVolumes.ts deleted file mode 100644 index 68c2d4c3..00000000 --- a/ui/packages/platform/src/api/cloud/getCloudVolumes.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { request } from 'helpers/request' - -export interface CloudVolumes { - api_name: string - type: string - cloud_provider: string - native_name: string - native_reference_price_per_1000gib_per_hour: number - native_reference_price_currency: string - native_reference_price_region: string - native_reference_price_revision_date: string -} - -export const getCloudVolumes = async (cloud_provider: string) => { - const response = await request( - `/cloud_volumes?cloud_provider=eq.${cloud_provider}`, - ) - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/cloud/getOrgKeys.ts b/ui/packages/platform/src/api/cloud/getOrgKeys.ts deleted file mode 100644 index 804befc5..00000000 --- a/ui/packages/platform/src/api/cloud/getOrgKeys.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { request } from 'helpers/request' - -export const getOrgKeys = async (org_id: number) => { - const response = await request(`/org_keys?org_id=eq.${org_id}`) - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/configs/getConfig.ts b/ui/packages/platform/src/api/configs/getConfig.ts deleted file mode 100644 index 22ed8fb4..00000000 --- a/ui/packages/platform/src/api/configs/getConfig.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { formatConfig } from '@postgres.ai/shared/types/api/entities/config' -import { request } from 'helpers/request' - -export const getConfig = async () => { - const response = await request('/admin/config') - - return { - response: response.ok ? formatConfig(await response.json()) : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/configs/getFullConfig.ts b/ui/packages/platform/src/api/configs/getFullConfig.ts deleted file mode 100644 index abf0338d..00000000 --- a/ui/packages/platform/src/api/configs/getFullConfig.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { request } from 'helpers/request' -export const getFullConfig = async () => { - const response = await request('/admin/config.yaml') - .then((res) => res.blob()) - .then((blob) => blob.text()) - .then((yamlAsString) => { - return yamlAsString - }) - - return { - response: response ? response : null, - error: response && null, - } -} diff --git a/ui/packages/platform/src/api/configs/getSeImages.ts b/ui/packages/platform/src/api/configs/getSeImages.ts deleted file mode 100644 index 17f2af97..00000000 --- a/ui/packages/platform/src/api/configs/getSeImages.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { request } from 'helpers/request' - -export const getSeImages = async ({ - packageGroup, - platformUrl, -}: { - packageGroup: string - platformUrl?: string -}) => { - const response = await request( - `/dblab_se_images?package_group=eq.${packageGroup} - `, - {}, - platformUrl, - ) - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/configs/getTaskState.ts b/ui/packages/platform/src/api/configs/getTaskState.ts deleted file mode 100644 index a614c105..00000000 --- a/ui/packages/platform/src/api/configs/getTaskState.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { simpleInstallRequest } from 'helpers/simpleInstallRequest' - -export const getTaskState = async (req: { taskID: string; userID?: number }) => { - const response = await simpleInstallRequest( - `/state/${req.taskID}`, - { - method: 'GET', - }, - req?.userID, - ) - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : await response.json(), - } -} diff --git a/ui/packages/platform/src/api/configs/initStreamLogs.ts b/ui/packages/platform/src/api/configs/initStreamLogs.ts deleted file mode 100644 index 4e010ccb..00000000 --- a/ui/packages/platform/src/api/configs/initStreamLogs.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { SI_API_SERVER } from 'helpers/simpleInstallRequest' - -export const initStreamLogs = (taskId: string, otCode: string): WebSocket => { - let url = new URL( - `${SI_API_SERVER.replace( - 'https', - 'wss', - )}/stream-logs/${taskId}?otCode=${otCode}`, - ) - return new WebSocket(url) -} diff --git a/ui/packages/platform/src/api/configs/launchDeploy.ts b/ui/packages/platform/src/api/configs/launchDeploy.ts deleted file mode 100644 index b593d98d..00000000 --- a/ui/packages/platform/src/api/configs/launchDeploy.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { - DEBUG_API_SERVER, - sePackageTag, -} from 'components/DbLabInstanceForm/utils' -import { simpleInstallRequest } from 'helpers/simpleInstallRequest' -import { useCloudProviderProps } from 'hooks/useCloudProvider' - -const API_SERVER = process.env.REACT_APP_API_SERVER - -const formatExtraEnvs = (extraEnvs: { [key: string]: string }) => { - return Object.entries(extraEnvs) - .filter(([key, value]) => value) - .map(([key, value]) => { - if (key === 'GCP_SERVICE_ACCOUNT_CONTENTS') { - return `${key}=${value.replace(/\n\s+/g, '')}` - } - return `${key}=${value}` - }) -} - -export const launchDeploy = async ({ - state, - userID, - orgKey, - extraEnvs, - cloudImage, - launchType, -}: { - state: useCloudProviderProps['initialState'] - orgKey: string - userID?: number - extraEnvs: { - [key: string]: string - } - cloudImage: string - launchType: 'cluster' | 'instance' -}) => { - const instanceExtraVars = [ - `provision=${state.provider}`, - `server_name=${state.name}`, - `platform_project_name=${state.name}`, - `server_type=${state.instanceType.native_name}`, - `server_image=${cloudImage}`, - `server_location=${state.location.native_code}`, - `volume_size=${state.storage}`, - `dblab_engine_version=${state.tag}`, - `zpool_datasets_number=${state.snapshots}`, - `dblab_engine_verification_token=${state.verificationToken}`, - `platform_org_key=${orgKey}`, - ] - - const instanceOptionalVars = [ - state.publicKeys && `ssh_public_keys="${state.publicKeys}"`, - API_SERVER === DEBUG_API_SERVER && - `platform_url=https://v2.postgres.ai/api/general`, - ].filter(Boolean) - - const instanceBody = { - playbook: 'deploy_dle.yml', - provision: state.provider, - server: { - name: state.name, - serverType: state.instanceType.native_name, - image: cloudImage, - location: state.location.native_code, - }, - image: `postgresai/dle-se-ansible:${sePackageTag}`, - extraVars: [...instanceExtraVars, ...instanceOptionalVars], - extraEnvs: formatExtraEnvs(extraEnvs), - } - - const user = state.provider === 'aws' ? 'ubuntu' : 'root' - - const extraVars = [ - `ansible_user=${user}`, - `provision=${state.provider}`, - `servers_count=${state.numberOfInstances}`, - `server_type=${state.instanceType.native_name}`, - `server_image=${cloudImage}`, - `server_location=${state.location.native_code}`, - `volume_size=${state.storage}`, - `postgresql_version=${state.version}`, - `database_public_access=${state.database_public_access}`, - `with_haproxy_load_balancing=${state.with_haproxy_load_balancing}`, - `pgbouncer_install=${state.pgbouncer_install}`, - `pg_data_mount_fstype=${state.fileSystem}`, - `synchronous_mode=${state.synchronous_mode}`, - `netdata_install=${state.netdata_install}`, - `patroni_cluster_name=${state.name}`, - `platform_org_key=${orgKey}`, - ] - - const optionalVars = [ - state.synchronous_mode && - `synchronous_node_count=${state.synchronous_node_count}`, - state.pg_repack && `enable_pg_repack=${state.pg_repack}`, - state.pg_cron && `enable_pg_cron=${state.pg_cron}`, - state.pgaudit && `enable_pgaudit=${state.pgaudit}`, - state.version !== 10 && - state.pgvector && - `enable_pgvector=${state.pgvector}`, - state.postgis && `enable_postgis=${state.postgis}`, - state.pgrouting && `enable_pgrouting=${state.pgrouting}`, - state.version !== 10 && - state.version !== 11 && - state.timescaledb && - `enable_timescaledb=${state.timescaledb}`, - state.version !== 10 && state.citus && `enable_citus=${state.citus}`, - state.pg_partman && `enable_pg_partman=${state.pg_partman}`, - state.pg_stat_kcache && `enable_pg_stat_kcache=${state.pg_stat_kcache}`, - state.pg_wait_sampling && - `enable_pg_wait_sampling=${state.pg_wait_sampling}`, - state.publicKeys && `ssh_public_keys="${state.publicKeys}"`, - API_SERVER === DEBUG_API_SERVER && - `platform_url=https://v2.postgres.ai/api/general`, - ].filter(Boolean) - - const clusterBody = { - playbook: 'deploy_pgcluster.yml', - provision: state.provider, - server: { - name: state.name, - serverType: state.instanceType.native_name, - image: cloudImage, - location: state.location.native_code, - }, - image: 'vitabaks/postgresql_cluster:cloud', - extraVars: [...extraVars, ...optionalVars], - extraEnvs: formatExtraEnvs(extraEnvs), - } - - const response = await simpleInstallRequest( - '/launch', - { - method: 'POST', - body: JSON.stringify( - launchType === 'cluster' ? clusterBody : instanceBody, - ), - }, - userID, - ) - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : await response.json(), - } -} diff --git a/ui/packages/platform/src/api/configs/regenerateCode.ts b/ui/packages/platform/src/api/configs/regenerateCode.ts deleted file mode 100644 index 0e15b0a2..00000000 --- a/ui/packages/platform/src/api/configs/regenerateCode.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { simpleInstallRequest } from 'helpers/simpleInstallRequest' - -export const regenerateCode = async (req: { - taskID: string - userID?: number -}) => { - const response = await simpleInstallRequest( - '/regenerate-code', - { - method: 'POST', - body: JSON.stringify({ - taskID: req.taskID, - }), - }, - req?.userID, - ) - - return { - response: response.ok ? await response.json() : null, - error: response.ok ? null : await response.json(), - } -} diff --git a/ui/packages/platform/src/api/configs/testDbSource.ts b/ui/packages/platform/src/api/configs/testDbSource.ts deleted file mode 100644 index 07817587..00000000 --- a/ui/packages/platform/src/api/configs/testDbSource.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { dbSource } from '@postgres.ai/shared/types/api/entities/dbSource' -import { request } from 'helpers/request' - -export const testDbSource = async (req: dbSource) => { - const response = await request('/admin/test-db-source', { - method: 'POST', - body: JSON.stringify({ - host: req.host, - port: req.port.toString(), - dbname: req.dbname, - username: req.username, - password: req.password, - db_list: req.db_list - }), - }) - - return { - response: response.ok ? await response.json(): null, - error: response.ok ? null : await response.json() - } -} diff --git a/ui/packages/platform/src/api/configs/updateConfig.ts b/ui/packages/platform/src/api/configs/updateConfig.ts deleted file mode 100644 index 9c40b4f1..00000000 --- a/ui/packages/platform/src/api/configs/updateConfig.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - postUniqueCustomOptions, - postUniqueDatabases, -} from '@postgres.ai/shared/pages/Configuration/utils' -import { Config } from '@postgres.ai/shared/types/api/entities/config' -import { request } from 'helpers/request' - -export const updateConfig = async (req: Config) => { - const response = await request('/admin/config', { - method: 'POST', - body: JSON.stringify({ - global: { - debug: req.debug, - }, - databaseContainer: { - dockerImage: req.dockerPath, - }, - databaseConfigs: { - configs: { - shared_buffers: req.sharedBuffers, - shared_preload_libraries: req.sharedPreloadLibraries, - ...(req.tuningParams as unknown as { [key: string]: string }), - }, - }, - retrieval: { - refresh: { - timetable: req.timetable, - }, - spec: { - logicalDump: { - options: { - databases: postUniqueDatabases(req.databases), - customOptions: postUniqueCustomOptions(req.pgDumpCustomOptions), - parallelJobs: req.dumpParallelJobs, - ignoreErrors: req.dumpIgnoreErrors, - source: { - connection: { - dbname: req.dbname, - host: req.host, - port: req.port, - username: req.username, - password: req.password, - }, - }, - }, - }, - logicalRestore: { - options: { - customOptions: postUniqueCustomOptions( - req.pgRestoreCustomOptions, - ), - parallelJobs: req.restoreParallelJobs, - ignoreErrors: req.restoreIgnoreErrors, - }, - }, - }, - }, - }), - }) - - return { - response: response.ok ? response : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/engine/getEngine.ts b/ui/packages/platform/src/api/engine/getEngine.ts deleted file mode 100644 index 59680981..00000000 --- a/ui/packages/platform/src/api/engine/getEngine.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { request } from 'helpers/request' -import { - EngineDto, - formatEngineDto, -} from '@postgres.ai/shared/types/api/endpoints/getEngine' - -export const getEngine = async () => { - const response = await request('/healthz') - - return { - response: response.ok - ? formatEngineDto((await response.json()) as EngineDto) - : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/engine/getWSToken.ts b/ui/packages/platform/src/api/engine/getWSToken.ts deleted file mode 100644 index 3a7872ce..00000000 --- a/ui/packages/platform/src/api/engine/getWSToken.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { request } from 'helpers/request' -import { formatWSTokenDto, WSTokenDTO } from '@postgres.ai/shared/types/api/entities/wsToken' -import { GetWSToken } from "@postgres.ai/shared/types/api/endpoints/getWSToken"; - -export const getWSToken: GetWSToken = async (req ) => { - const response = await request('/admin/ws-auth') - - return { - response: response.ok - ? formatWSTokenDto((await response.json()) as WSTokenDTO) - : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/engine/initWS.ts b/ui/packages/platform/src/api/engine/initWS.ts deleted file mode 100644 index 74fd0164..00000000 --- a/ui/packages/platform/src/api/engine/initWS.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { InitWS } from "@postgres.ai/shared/types/api/endpoints/initWS"; -import { WS_URL_PREFIX } from 'config/env' - -export const initWS: InitWS = (path: string, token: string): WebSocket => { - let url = new URL(WS_URL_PREFIX + path, window.location.href); - url.protocol = url.protocol.replace('http', 'ws'); - const wsAddr = url.href + '?token=' + token; - - return new WebSocket(wsAddr) -} diff --git a/ui/packages/platform/src/api/explain/depesz.js b/ui/packages/platform/src/api/explain/depesz.js deleted file mode 100644 index 13dde5b0..00000000 --- a/ui/packages/platform/src/api/explain/depesz.js +++ /dev/null @@ -1,36 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import 'es6-promise/auto'; -import 'whatwg-fetch'; - -class ExplainDepeszApi { - constructor(setting) { - this.server = setting.explainDepeszServer; - } - - post(url, data, options = {}) { - let fetchOptions = { - ...options, - method: 'post', - body: data - }; - - return fetch(url, fetchOptions); - } - - postPlan(plan, query) { - const formData = new FormData(); - formData.append('is_public', '0'); - formData.append('plan', plan); - formData.append('query', query); - - return this.post(this.server, formData); - } -} - -export default ExplainDepeszApi; diff --git a/ui/packages/platform/src/api/explain/pev2.js b/ui/packages/platform/src/api/explain/pev2.js deleted file mode 100644 index 92ceb51f..00000000 --- a/ui/packages/platform/src/api/explain/pev2.js +++ /dev/null @@ -1,37 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import 'es6-promise/auto'; -import 'whatwg-fetch'; - -class ExplainPev2Api { - constructor(setting) { - this.server = setting.explainPev2Server; - } - - post(url, data, options = {}) { - let fetchOptions = { - ...options, - method: 'post', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }; - - return fetch(url, fetchOptions); - } - - postPlan(plan, query) { - return this.post(`${this.server}/api/rpc/post_plan`, { - plan: plan, - query: query || '' - }); - } -} - -export default ExplainPev2Api; diff --git a/ui/packages/platform/src/api/getMeta.ts b/ui/packages/platform/src/api/getMeta.ts deleted file mode 100644 index d88573c6..00000000 --- a/ui/packages/platform/src/api/getMeta.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { request } from '@postgres.ai/shared/helpers/request' - -import { MetaDto, formatMetaDto } from 'types/api/entities/meta' - -export const getMeta = async () => { - const response = await request('/meta.json') - - return { - response: response.ok ? formatMetaDto(await response.json() as MetaDto) : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/instances/getInstance.ts b/ui/packages/platform/src/api/instances/getInstance.ts deleted file mode 100644 index fdaf3354..00000000 --- a/ui/packages/platform/src/api/instances/getInstance.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { - formatInstanceDto, - InstanceDto, -} from '@postgres.ai/shared/types/api/entities/instance' -import { GetInstance } from '@postgres.ai/shared/types/api/endpoints/getInstance' - -import { request } from 'helpers/request' - -export const getInstance: GetInstance = async (req) => { - const response = await request('/dblab_instances', { - params: { - id: `eq.${req.instanceId}`, - }, - }) - - return { - response: response.ok - ? ((await response.json()) as InstanceDto[]).map(formatInstanceDto)[0] ?? - null - : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/instances/getWSToken.ts b/ui/packages/platform/src/api/instances/getWSToken.ts deleted file mode 100644 index d5a67a3e..00000000 --- a/ui/packages/platform/src/api/instances/getWSToken.ts +++ /dev/null @@ -1,59 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2022, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { request } from 'helpers/request'; -import { localStorage } from "helpers/localStorage"; - -import { GetWSToken } from "@postgres.ai/shared/types/api/endpoints/getWSToken"; -import { formatWSTokenDto, WSTokenDTO } from "@postgres.ai/shared/types/api/entities/wsToken"; -import { request as requestCore } from "@postgres.ai/shared/helpers/request"; -import { formatInstanceDto, InstanceDto } from "@postgres.ai/shared/types/api/entities/instance"; - -export const getWSToken: GetWSToken = async (req) => { - // TODO: define instance and get a websocket token. - const instanceResponse = await request('/dblab_instances', { - params: { - id: `eq.${req.instanceId}`, - }, - }) - - if (!instanceResponse.ok) { - return { - response: null, - error: instanceResponse, - } - } - - const instance = (await instanceResponse.json() as InstanceDto[]).map(formatInstanceDto)[0] - - const authToken = localStorage.getAuthToken() - - if (instance.useTunnel) { - return { - response: null, - error: new Response(null, { - status: 400, - statusText: `Cannot connect to an instance that is using a tunnel`, - }) - } - } - - const response = await requestCore(instance.url + '/admin/ws-auth', { - headers: { - ...(authToken && {'Verification-Token': authToken}), - }, - }) - - return { - response: response.ok - ? formatWSTokenDto((await response.json()) as WSTokenDTO) - : null, - error: response.ok ? null : response, - } -} - - diff --git a/ui/packages/platform/src/api/instances/refreshInstance.ts b/ui/packages/platform/src/api/instances/refreshInstance.ts deleted file mode 100644 index 92777110..00000000 --- a/ui/packages/platform/src/api/instances/refreshInstance.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { RefreshInstance } from '@postgres.ai/shared/types/api/endpoints/refreshInstance' - -import { request } from 'helpers/request' - -export const refreshInstance: RefreshInstance = async (req) => { - const response = await request('/rpc/dblab_instance_status_refresh', { - method: 'post', - body: JSON.stringify({ - instance_id: req.instanceId, - }), - }) - - return { - response: response.ok ? true : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/api/snapshots/getSnapshots.ts b/ui/packages/platform/src/api/snapshots/getSnapshots.ts deleted file mode 100644 index 35d08eb3..00000000 --- a/ui/packages/platform/src/api/snapshots/getSnapshots.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { - SnapshotDto, - formatSnapshotDto, -} from '@postgres.ai/shared/types/api/entities/snapshot' -import { GetSnapshots } from '@postgres.ai/shared/types/api/endpoints/getSnapshots' - -import { request } from 'helpers/request' - -export const getSnapshots: GetSnapshots = async (req) => { - const response = await request('/rpc/dblab_instance_snapshots', { - method: 'POST', - body: JSON.stringify({ - instance_id: req.instanceId, - }), - }) - - return { - response: response.ok - ? ((await response.json()) as SnapshotDto[]).map(formatSnapshotDto) - : null, - error: response.ok ? null : response, - } -} diff --git a/ui/packages/platform/src/assets/explainSamples.ts b/ui/packages/platform/src/assets/explainSamples.ts deleted file mode 100644 index 87590585..00000000 --- a/ui/packages/platform/src/assets/explainSamples.ts +++ /dev/null @@ -1,600 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -const sampleExplain1 = - `[ - { - "Plan": { - "Node Type": "Limit", - "Startup Cost": 17024.84, - "Total Cost": 17024.87, - "Plan Rows": 10, - "Plan Width": 133, - "Actual Startup Time": 725.773, - "Actual Total Time": 725.775, - "Actual Rows": 10, - "Actual Loops": 1, - "Output": ["c.state", "cat.categoryname", "(sum(o.netamount))", "(sum(o.totalamount))"], - "Shared Hit Blocks": 23, - "Shared Read Blocks": 1392, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Sort", - "Parent Relationship": "Outer", - "Startup Cost": 17024.84, - "Total Cost": 17026.88, - "Plan Rows": 816, - "Plan Width": 133, - "Actual Startup Time": 725.771, - "Actual Total Time": 725.772, - "Actual Rows": 11, - "Actual Loops": 1, - "Output": ["c.state", "cat.categoryname", "(sum(o.netamount))", "(sum(o.totalamount))"], - "Sort Key": ["c.state", "(sum(o.totalamount))"], - "Sort Method": "top-N heapsort", - "Sort Space Used": 25, - "Sort Space Type": "Memory", - "Shared Hit Blocks": 23, - "Shared Read Blocks": 1392, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Aggregate", - "Strategy": "Hashed", - "Parent Relationship": "Outer", - "Startup Cost": 16994.41, - "Total Cost": 17006.65, - "Plan Rows": 816, - "Plan Width": 133, - "Actual Startup Time": 723.877, - "Actual Total Time": 724.417, - "Actual Rows": 832, - "Actual Loops": 1, - "Output": ["c.state", "cat.categoryname", "sum(o.netamount)", "sum(o.totalamount)"], - "Group Key": ["c.state", "cat.categoryname"], - "Shared Hit Blocks": 13, - "Shared Read Blocks": 1392, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Hash Join", - "Parent Relationship": "Outer", - "Join Type": "Inner", - "Startup Cost": 4966.48, - "Total Cost": 13742.65, - "Plan Rows": 325176, - "Plan Width": 133, - "Actual Startup Time": 118.314, - "Actual Total Time": 354.285, - "Actual Rows": 383270, - "Actual Loops": 1, - "Output": ["c.state", "o.netamount", "o.totalamount", "cat.categoryname"], - "Hash Cond": "(o.orderid = ch.orderid)", - "Shared Hit Blocks": 13, - "Shared Read Blocks": 1392, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Hash Join", - "Parent Relationship": "Outer", - "Join Type": "Inner", - "Startup Cost": 834.86, - "Total Cost": 4539.11, - "Plan Rows": 60350, - "Plan Width": 138, - "Actual Startup Time": 22.651, - "Actual Total Time": 133.484, - "Actual Rows": 60350, - "Actual Loops": 1, - "Output": [ - "o.netamount", - "o.totalamount", - "o.orderid", - "ol.orderid", - "cat.categoryname" - ], - "Hash Cond": "(ol.orderid = o.orderid)", - "Shared Hit Blocks": 9, - "Shared Read Blocks": 581, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Hash Join", - "Parent Relationship": "Outer", - "Join Type": "Inner", - "Startup Cost": 464.86, - "Total Cost": 2962.11, - "Plan Rows": 60350, - "Plan Width": 122, - "Actual Startup Time": 12.467, - "Actual Total Time": 85.647, - "Actual Rows": 60350, - "Actual Loops": 1, - "Output": ["ol.orderid", "cat.categoryname"], - "Hash Cond": "(ol.prod_id = p.prod_id)", - "Shared Hit Blocks": 4, - "Shared Read Blocks": 483, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Seq Scan", - "Parent Relationship": "Outer", - "Relation Name": "orderlines", - "Schema": "public", - "Alias": "ol", - "Startup Cost": 0.00, - "Total Cost": 988.50, - "Plan Rows": 60350, - "Plan Width": 8, - "Actual Startup Time": 0.005, - "Actual Total Time": 14.054, - "Actual Rows": 60350, - "Actual Loops": 1, - "Output": [ - "ol.orderlineid", - "ol.orderid", - "ol.prod_id", - "ol.quantity", - "ol.orderdate" - ], - "Shared Hit Blocks": 2, - "Shared Read Blocks": 383, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000 - }, - { - "Node Type": "Hash", - "Parent Relationship": "Inner", - "Startup Cost": 339.86, - "Total Cost": 339.86, - "Plan Rows": 10000, - "Plan Width": 122, - "Actual Startup Time": 12.446, - "Actual Total Time": 12.446, - "Actual Rows": 10000, - "Actual Loops": 1, - "Output": ["p.prod_id", "cat.categoryname"], - "Hash Buckets": 1024, - "Hash Batches": 1, - "Original Hash Batches": 1, - "Peak Memory Usage": 425, - "Shared Hit Blocks": 2, - "Shared Read Blocks": 100, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Hash Join", - "Parent Relationship": "Outer", - "Join Type": "Inner", - "Startup Cost": 1.36, - "Total Cost": 339.86, - "Plan Rows": 10000, - "Plan Width": 122, - "Actual Startup Time": 0.283, - "Actual Total Time": 9.015, - "Actual Rows": 10000, - "Actual Loops": 1, - "Output": ["p.prod_id", "cat.categoryname"], - "Hash Cond": "(p.category = cat.category)", - "Shared Hit Blocks": 2, - "Shared Read Blocks": 100, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Seq Scan", - "Parent Relationship": "Outer", - "Relation Name": "products", - "Schema": "public", - "Alias": "p", - "Startup Cost": 0.00, - "Total Cost": 201.00, - "Plan Rows": 10000, - "Plan Width": 8, - "Actual Startup Time": 0.003, - "Actual Total Time": 4.330, - "Actual Rows": 10000, - "Actual Loops": 1, - "Output": [ - "p.prod_id", - "p.category", - "p.title", - "p.actor", - "p.price", - "p.special", - "p.common_prod_id" - ], - "Shared Hit Blocks": 2, - "Shared Read Blocks": 99, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000 - }, - { - "Node Type": "Hash", - "Parent Relationship": "Inner", - "Startup Cost": 1.16, - "Total Cost": 1.16, - "Plan Rows": 16, - "Plan Width": 122, - "Actual Startup Time": 0.265, - "Actual Total Time": 0.265, - "Actual Rows": 16, - "Actual Loops": 1, - "Output": [ - "cat.categoryname", - "cat.category" - ], - "Hash Buckets": 1024, - "Hash Batches": 1, - "Original Hash Batches": 1, - "Peak Memory Usage": 1, - "Shared Hit Blocks": 0, - "Shared Read Blocks": 1, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Seq Scan", - "Parent Relationship": "Outer", - "Relation Name": "categories", - "Schema": "public", - "Alias": "cat", - "Startup Cost": 0.00, - "Total Cost": 1.16, - "Plan Rows": 16, - "Plan Width": 122, - "Actual Startup Time": 0.250, - "Actual Total Time": 0.252, - "Actual Rows": 16, - "Actual Loops": 1, - "Output": ["cat.categoryname", "cat.category"], - "Shared Hit Blocks": 0, - "Shared Read Blocks": 1, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000 - } - ] - } - ] - } - ] - } - ] - }, - { - "Node Type": "Hash", - "Parent Relationship": "Inner", - "Startup Cost": 220.00, - "Total Cost": 220.00, - "Plan Rows": 12000, - "Plan Width": 16, - "Actual Startup Time": 10.159, - "Actual Total Time": 10.159, - "Actual Rows": 12000, - "Actual Loops": 1, - "Output": ["o.netamount", "o.totalamount", "o.orderid"], - "Hash Buckets": 2048, - "Hash Batches": 1, - "Original Hash Batches": 1, - "Peak Memory Usage": 609, - "Shared Hit Blocks": 2, - "Shared Read Blocks": 98, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Seq Scan", - "Parent Relationship": "Outer", - "Relation Name": "orders", - "Schema": "public", - "Alias": "o", - "Startup Cost": 0.00, - "Total Cost": 220.00, - "Plan Rows": 12000, - "Plan Width": 16, - "Actual Startup Time": 0.008, - "Actual Total Time": 5.548, - "Actual Rows": 12000, - "Actual Loops": 1, - "Output": ["o.netamount", "o.totalamount", "o.orderid"], - "Shared Hit Blocks": 2, - "Shared Read Blocks": 98, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000 - } - ] - } - ] - }, - { - "Node Type": "Hash", - "Parent Relationship": "Inner", - "Startup Cost": 3377.25, - "Total Cost": 3377.25, - "Plan Rows": 60350, - "Plan Width": 7, - "Actual Startup Time": 95.610, - "Actual Total Time": 95.610, - "Actual Rows": 60350, - "Actual Loops": 1, - "Output": ["c.state", "ch.orderid"], - "Hash Buckets": 8192, - "Hash Batches": 1, - "Original Hash Batches": 1, - "Peak Memory Usage": 2239, - "Shared Hit Blocks": 4, - "Shared Read Blocks": 811, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Hash Join", - "Parent Relationship": "Outer", - "Join Type": "Inner", - "Startup Cost": 938.00, - "Total Cost": 3377.25, - "Plan Rows": 60350, - "Plan Width": 7, - "Actual Startup Time": 24.115, - "Actual Total Time": 74.639, - "Actual Rows": 60350, - "Actual Loops": 1, - "Output": ["c.state", "ch.orderid"], - "Hash Cond": "(ch.customerid = c.customerid)", - "Shared Hit Blocks": 4, - "Shared Read Blocks": 811, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Seq Scan", - "Parent Relationship": "Outer", - "Relation Name": "cust_hist", - "Schema": "public", - "Alias": "ch", - "Startup Cost": 0.00, - "Total Cost": 930.50, - "Plan Rows": 60350, - "Plan Width": 8, - "Actual Startup Time": 0.294, - "Actual Total Time": 11.812, - "Actual Rows": 60350, - "Actual Loops": 1, - "Output": ["ch.customerid", "ch.orderid", "ch.prod_id"], - "Shared Hit Blocks": 2, - "Shared Read Blocks": 325, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000 - }, - { - "Node Type": "Hash", - "Parent Relationship": "Inner", - "Startup Cost": 688.00, - "Total Cost": 688.00, - "Plan Rows": 20000, - "Plan Width": 7, - "Actual Startup Time": 23.786, - "Actual Total Time": 23.786, - "Actual Rows": 20000, - "Actual Loops": 1, - "Output": ["c.state", "c.customerid"], - "Hash Buckets": 2048, - "Hash Batches": 1, - "Original Hash Batches": 1, - "Peak Memory Usage": 743, - "Shared Hit Blocks": 2, - "Shared Read Blocks": 486, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000, - "Plans": [ - { - "Node Type": "Seq Scan", - "Parent Relationship": "Outer", - "Relation Name": "customers", - "Schema": "public", - "Alias": "c", - "Startup Cost": 0.00, - "Total Cost": 688.00, - "Plan Rows": 20000, - "Plan Width": 7, - "Actual Startup Time": 0.005, - "Actual Total Time": 16.771, - "Actual Rows": 20000, - "Actual Loops": 1, - "Output": ["c.state", "c.customerid"], - "Shared Hit Blocks": 2, - "Shared Read Blocks": 486, - "Shared Dirtied Blocks": 0, - "Shared Written Blocks": 0, - "Local Hit Blocks": 0, - "Local Read Blocks": 0, - "Local Dirtied Blocks": 0, - "Local Written Blocks": 0, - "Temp Read Blocks": 0, - "Temp Written Blocks": 0, - "I/O Read Time": 0.000, - "I/O Write Time": 0.000 - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - }, - "Planning Time": 26.171, - "Triggers": [ - ], - "Execution Time": 726.800 - } -]`; - -const explainSamples = [{ - value: sampleExplain1, - label: 'Sample 1' -}]; - -export default explainSamples; diff --git a/ui/packages/platform/src/assets/messages.ts b/ui/packages/platform/src/assets/messages.ts deleted file mode 100644 index c136a318..00000000 --- a/ui/packages/platform/src/assets/messages.ts +++ /dev/null @@ -1,11 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -export const messages = { - noPermission: 'You do not have permission to do this.', - noPermissionPage: 'You do not have permission to view this page.', -} diff --git a/ui/packages/platform/src/assets/plans.ts b/ui/packages/platform/src/assets/plans.ts deleted file mode 100644 index 86d0b075..00000000 --- a/ui/packages/platform/src/assets/plans.ts +++ /dev/null @@ -1,26 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { colors } from '@postgres.ai/shared/styles/colors' - -export const plans = { - ce: { - id: 'ce', - name: 'CE', - title: 'Postgres.ai Community Edition', - color: colors.secondary2.main, - limits: { - maxDblabInstances: 1, - maxJoeInstances: 1, - daysJoeHistory: 14, - emailDomainRestricted: true, - }, - }, - ee_gold_monthly: { - color: colors.pgaiOrange, - }, -} diff --git a/ui/packages/platform/src/assets/visualizeTypes.ts b/ui/packages/platform/src/assets/visualizeTypes.ts deleted file mode 100644 index 42ddfc93..00000000 --- a/ui/packages/platform/src/assets/visualizeTypes.ts +++ /dev/null @@ -1,12 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -export const visualizeTypes = { - depesz: 'depesz', - pev2: 'pev2', - flame: 'flame-graph', -} diff --git a/ui/packages/platform/src/components/AccessTokens/AccessTokens.tsx b/ui/packages/platform/src/components/AccessTokens/AccessTokens.tsx deleted file mode 100644 index 734e24bb..00000000 --- a/ui/packages/platform/src/components/AccessTokens/AccessTokens.tsx +++ /dev/null @@ -1,545 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component, MouseEvent } from 'react' -import { - Table, - TableBody, - TableCell, - TableHead, - TableRow, - TextField, - Button, - FormControlLabel, - Checkbox, -} from '@material-ui/core' - -import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer' -import { styles } from '@postgres.ai/shared/styles/styles' -import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner' -import { - ClassesType, - RefluxTypes, - TokenRequestProps, -} from '@postgres.ai/platform/src/components/types' - -import Store from '../../stores/store' -import Actions from '../../actions/actions' -import { ErrorWrapper } from 'components/Error/ErrorWrapper' -import ConsolePageTitle from '../ConsolePageTitle' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { DisplayTokenWrapper } from 'components/DisplayToken/DisplayTokenWrapper' -import { AccessTokensProps } from 'components/AccessTokens/AccessTokensWrapper' -import { FilteredTableMessage } from 'components/AccessTokens/FilteredTableMessage/FilteredTableMessage' - -interface AccessTokensWithStylesProps extends AccessTokensProps { - classes: ClassesType -} - -interface UserTokenData { - id: number - name: string - is_personal: boolean - username: string - created_formatted: string - expires_formatted: string - revoking: boolean -} - -interface AccessTokensState { - filterValue: string - data: { - auth: { - token: string - } | null - userTokens: { - orgId: number | null - isProcessing: boolean - isProcessed: boolean - data: UserTokenData[] - error: { - message: boolean - } - } - tokenRequest: TokenRequestProps - } - tokenName: string | null - tokenExpires: string | null - processed: boolean - isPersonal: boolean -} - -class AccessTokens extends Component< - AccessTokensWithStylesProps, - AccessTokensState -> { - state = { - filterValue: '', - data: { - auth: { - token: '', - }, - userTokens: { - orgId: null, - isProcessing: false, - isProcessed: false, - data: [], - error: { - message: false, - }, - }, - tokenRequest: { - isProcessing: false, - isProcessed: false, - data: { - name: '', - is_personal: false, - expires_at: '', - token: '', - }, - errorMessage: '', - error: false, - }, - }, - tokenName: '', - tokenExpires: '', - processed: false, - isPersonal: true, - } - - handleChange = (event: React.ChangeEvent) => { - const name = event.target.name - const value = event.target.value - - if (name === 'tokenName') { - this.setState({ tokenName: value }) - } else if (name === 'tokenExpires') { - this.setState({ tokenExpires: value }) - } else if (name === 'isPersonal') { - this.setState({ isPersonal: event.target.checked }) - } - } - unsubscribe: Function - componentDidMount() { - const that = this - const orgId = this.props.orgId ? this.props.orgId : null - const date = new Date() - const expiresDate = - date.getFullYear() + - 1 + - '-' + - ('0' + (date.getMonth() + 1)).slice(-2) + - '-' + - ('0' + date.getDate()).slice(-2) - - document.getElementsByTagName('html')[0].style.overflow = 'hidden' - - this.unsubscribe = (Store.listen as RefluxTypes["listen"]) (function () { - const auth: AccessTokensState['data']['auth'] = - this.data && this.data.auth ? this.data.auth : null - const userTokens: AccessTokensState['data']['userTokens'] = - this.data && this.data.userTokens ? this.data.userTokens : null - const tokenRequest: TokenRequestProps = - this.data && this.data.tokenRequest ? this.data.tokenRequest : null - - that.setState({ data: this.data }) - - if ( - auth && - auth.token && - (!userTokens.isProcessed || orgId !== userTokens.orgId) && - !userTokens.isProcessing && - !userTokens.error - ) { - Actions.getAccessTokens(auth.token, orgId) - } - - if ( - tokenRequest && - tokenRequest.isProcessed && - !tokenRequest.error && - tokenRequest.data && - tokenRequest.data.name === that.state.tokenName && - tokenRequest.data.expires_at && - tokenRequest.data.token - ) { - that.setState({ - tokenName: '', - tokenExpires: expiresDate, - processed: false, - isPersonal: true, - }) - } - }) - - that.setState({ - tokenName: '', - tokenExpires: expiresDate, - processed: false, - }) - - Actions.refresh() - } - - componentWillUnmount() { - Actions.hideGeneratedAccessToken() - this.unsubscribe() - } - - addToken = () => { - const orgId = this.props.orgId ? this.props.orgId : null - const auth = - this.state.data && this.state.data.auth ? this.state.data.auth : null - const tokenRequest = - this.state.data && this.state.data.tokenRequest - ? this.state.data.tokenRequest - : null - - if ( - this.state.tokenName === null || - this.state.tokenName === '' || - this.state.tokenExpires === null || - this.state.tokenExpires === '' - ) { - this.setState({ processed: true }) - return - } - - if (auth && auth.token && !tokenRequest?.isProcessing) { - Actions.getAccessToken( - auth.token, - this.state.tokenName, - this.state.tokenExpires, - orgId, - this.state.isPersonal, - ) - } - } - - getTodayDate() { - const date = new Date() - - return ( - date.getFullYear() + - '-' + - ('0' + (date.getMonth() + 1)).slice(-2) + - '-' + - ('0' + date.getDate()).slice(-2) - ) - } - - revokeToken = ( - _event: MouseEvent, - id: number, - name: string, - ) => { - const orgId = this.props.orgId ? this.props.orgId : null - const auth = - this.state.data && this.state.data.auth ? this.state.data.auth : null - - /* eslint no-alert: 0 */ - if ( - window.confirm( - 'Are you sure you want to revoke token "' + name + '"?', - ) === true - ) { - Actions.revokeAccessToken(auth?.token, orgId, id) - } - } - - filterTokensInputHandler = (event: React.ChangeEvent) => { - this.setState({ filterValue: event.target.value }) - } - - render() { - const { classes, orgPermissions, orgId } = this.props - const data = - this.state && this.state.data ? this.state.data.userTokens : null - const tokenRequest = - this.state && this.state.data && this.state.data.tokenRequest - ? this.state.data.tokenRequest - : null - const filteredTokens = data?.data?.filter( - (token: UserTokenData) => - token.name - ?.toLowerCase() - .indexOf((this.state.filterValue || '')?.toLowerCase()) !== -1, - ) - - const pageTitle = ( - 0 - ? { - filterValue: this.state.filterValue, - filterHandler: this.filterTokensInputHandler, - placeholder: 'Search access tokens by name', - } - : null - } - /> - ) - - let tokenDisplay = null - if ( - tokenRequest && - tokenRequest.isProcessed && - !tokenRequest.error && - tokenRequest.data && - tokenRequest.data.name && - tokenRequest.data.expires_at && - tokenRequest.data.token - ) { - tokenDisplay = ( -
-

- {tokenRequest.data.is_personal - ? 'Your new personal access token' - : 'New administrative access token'} -

- -
- ) - } - - let tokenError = null - if (tokenRequest && tokenRequest.error) { - tokenError = ( -
{tokenRequest.errorMessage}
- ) - } - - const tokenForm = ( -
-

Add token

-
- {tokenError} - - - - - - - } - label="Personal token" - /> - - -
-
- ) - - const breadcrumbs = ( - - ) - - if (this.state && this.state.data && this.state.data.userTokens?.error) { - return ( -
- {breadcrumbs} - - {pageTitle} - -

Access tokens

- -
- ) - } - - if ( - !data || - (data && data.isProcessing) || - (data && data.orgId !== orgId) - ) { - return ( -
- {breadcrumbs} - {pageTitle} - - -
- ) - } - - return ( -
- {breadcrumbs} - - {pageTitle} - - {tokenDisplay} - - {tokenForm} - -
- Users may manage their personal tokens only. Admins may manage their -  personal tokens, as well as administrative (impersonal) tokens -  used to organize infrastructure. Tokens of all types work in -  the context of a particular organization. -
- -
-

Active access tokens

- - {filteredTokens && filteredTokens.length > 0 ? ( - - - - - Name - Type - Creator - Created - Expires - Actions - - - - - {filteredTokens && - filteredTokens.length > 0 && - filteredTokens.map((t: UserTokenData) => { - return ( - - - {t.name} - - - {t.is_personal ? 'Personal' : 'Administrative'} - - - {t.username} - - - {t.created_formatted} - - - {t.expires_formatted} - - - - - - ) - })} - -
-
- ) : ( - - this.setState({ - filterValue: '', - }) - } - /> - )} - -
-
- ) - } -} - -export default AccessTokens diff --git a/ui/packages/platform/src/components/AccessTokens/AccessTokensWrapper.tsx b/ui/packages/platform/src/components/AccessTokens/AccessTokensWrapper.tsx deleted file mode 100644 index 237e8e47..00000000 --- a/ui/packages/platform/src/components/AccessTokens/AccessTokensWrapper.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import AccessTokens from 'components/AccessTokens/AccessTokens' -import { OrgPermissions } from 'components/types' -import { styles } from '@postgres.ai/shared/styles/styles' - -export interface AccessTokensProps { - project: string | undefined - orgId: number - org: string | number - orgPermissions: OrgPermissions -} - -export const AccessTokensWrapper = (props: AccessTokensProps) => { - const useStyles = makeStyles( - (theme) => ({ - root: { - ...(styles.root as Object), - display: 'flex', - flexDirection: 'column', - paddingBottom: '20px', - }, - container: { - display: 'flex', - flexWrap: 'wrap', - }, - textField: { - ...styles.inputField, - maxWidth: 400, - marginBottom: 15, - marginRight: theme.spacing(1), - marginTop: '16px', - }, - nameField: { - ...styles.inputField, - maxWidth: 400, - marginBottom: 15, - width: '400px', - marginRight: theme.spacing(1), - }, - addTokenButton: { - marginTop: 15, - height: '33px', - marginBottom: 10, - maxWidth: 'max-content', - }, - revokeButton: { - paddingRight: 5, - paddingLeft: 5, - paddingTop: 3, - paddingBottom: 3, - }, - errorMessage: { - color: 'red', - width: '100%', - }, - remark: { - width: '100%', - maxWidth: 960, - }, - bottomSpace: { - ...styles.bottomSpace, - }, - }), - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/AccessTokens/FilteredTableMessage/FilteredTableMessage.tsx b/ui/packages/platform/src/components/AccessTokens/FilteredTableMessage/FilteredTableMessage.tsx deleted file mode 100644 index c10af14b..00000000 --- a/ui/packages/platform/src/components/AccessTokens/FilteredTableMessage/FilteredTableMessage.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Button } from '@material-ui/core' -import { AuditLogData } from 'components/Audit/Audit' - -export const FilteredTableMessage = ({ - filterValue, - filteredItems, - clearFilter, - emptyState, -}: { - filterValue: string - filteredItems: string[] | never[] | AuditLogData[] | undefined | null - clearFilter: () => void - emptyState: string | JSX.Element -}) => { - if (filterValue && filteredItems?.length === 0) { - return ( - <> -
- No results found for {filterValue} -
- - - ) - } - - return <>{emptyState} -} diff --git a/ui/packages/platform/src/components/AddDbLabInstanceFormWrapper/AddDbLabInstanceFormWrapper.tsx b/ui/packages/platform/src/components/AddDbLabInstanceFormWrapper/AddDbLabInstanceFormWrapper.tsx deleted file mode 100644 index 8800ba33..00000000 --- a/ui/packages/platform/src/components/AddDbLabInstanceFormWrapper/AddDbLabInstanceFormWrapper.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import { styles } from '@postgres.ai/shared/styles/styles' -import AddDbLabInstanceForm from 'components/AddDbLabInstanceFormWrapper/AddDblabInstanceForm' -import { OrgPermissions } from 'components/types' -import { RouteComponentProps } from 'react-router' - -export interface DbLabInstanceFormProps { - edit?: boolean - orgId: number - project: string | undefined - history: RouteComponentProps['history'] - orgPermissions: OrgPermissions -} - -export const AddDbLabInstanceFormWrapper = (props: DbLabInstanceFormProps) => { - const useStyles = makeStyles( - { - textField: { - ...styles.inputField, - maxWidth: 400, - }, - errorMessage: { - marginTop: 10, - color: 'red', - }, - fieldBlock: { - width: '100%', - }, - urlOkIcon: { - color: 'green', - }, - urlOk: { display: 'flex', gap: 5, alignItems: 'center', color: 'green' }, - urlTextMargin: { - marginTop: 10, - }, - urlFailIcon: { - color: 'red', - }, - urlFail: { - display: 'flex', - gap: 5, - alignItems: 'center', - color: 'red', - }, - warning: { - color: '#801200', - fontSize: '0.9em', - }, - warningIcon: { - color: '#801200', - fontSize: '1.2em', - position: 'relative', - marginBottom: -3, - }, - }, - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/AddDbLabInstanceFormWrapper/AddDblabInstanceForm.tsx b/ui/packages/platform/src/components/AddDbLabInstanceFormWrapper/AddDblabInstanceForm.tsx deleted file mode 100644 index 00746b31..00000000 --- a/ui/packages/platform/src/components/AddDbLabInstanceFormWrapper/AddDblabInstanceForm.tsx +++ /dev/null @@ -1,646 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component } from 'react' -import { - Checkbox, - Grid, - Button, - TextField, - FormControlLabel, -} from '@material-ui/core' -import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline' -import BlockIcon from '@material-ui/icons/Block' -import WarningIcon from '@material-ui/icons/Warning' - -import { styles } from '@postgres.ai/shared/styles/styles' -import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner' -import { - ClassesType, - ProjectProps, - RefluxTypes, -} from '@postgres.ai/platform/src/components/types' - -import Actions from '../../actions/actions' -import ConsolePageTitle from './../ConsolePageTitle' -import Store from '../../stores/store' -import Urls from '../../utils/urls' -import { generateToken, isHttps } from '../../utils/utils' -import { WarningWrapper } from 'components/Warning/WarningWrapper' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { DbLabInstanceFormProps } from 'components/DbLabInstanceForm/DbLabInstanceFormWrapper' - -interface DbLabInstanceFormWithStylesProps extends DbLabInstanceFormProps { - classes: ClassesType -} - -interface DbLabInstanceFormState { - data: { - auth: { - token: string | null - } | null - projects: ProjectProps - newDbLabInstance: { - isUpdating: boolean - isChecking: boolean - isChecked: boolean - isCheckProcessed: boolean - errorMessage: string - error: boolean - isProcessed: boolean - data: { - id: string - } - } | null - dbLabInstances: { - isProcessing: boolean - error: boolean - isProcessed: boolean - data: unknown - } | null - } | null - url: string - token: string | null - useTunnel: boolean - instanceID: string - project: string - project_label: string - errorFields: string[] - sshServerUrl: string -} - -class DbLabInstanceForm extends Component< - DbLabInstanceFormWithStylesProps, - DbLabInstanceFormState -> { - state = { - url: 'https://', - token: null, - useTunnel: false, - instanceID: '', - project: this.props.project ? this.props.project : '', - project_label: '', - errorFields: [''], - sshServerUrl: '', - data: { - auth: { - token: null, - }, - projects: { - data: [], - error: false, - isProcessing: false, - isProcessed: false, - }, - newDbLabInstance: { - isUpdating: false, - isChecked: false, - isChecking: false, - isCheckProcessed: false, - isProcessed: false, - error: false, - errorMessage: '', - data: { - id: '', - }, - }, - dbLabInstances: { - isProcessing: false, - error: false, - isProcessed: false, - data: '', - }, - }, - } - - unsubscribe: Function - componentDidMount() { - const that = this - const { orgId } = this.props - const url = window.location.href.split('/') - const instanceID = url[url.length - 1] - - this.unsubscribe = (Store.listen as RefluxTypes['listen'])(function () { - that.setState({ data: this.data, instanceID: instanceID }) - - const auth = this.data && this.data.auth ? this.data.auth : null - const projects = - this.data && this.data.projects ? this.data.projects : null - const dbLabInstances = - this.data && this.data.dbLabInstances ? this.data.dbLabInstances : null - - if (dbLabInstances.data) { - that.setState({ - project_label: - that.state.project_label || - dbLabInstances.data[instanceID]?.project_label_or_name, - token: - that.state.token || dbLabInstances.data[instanceID]?.verify_token, - useTunnel: - that.state.useTunnel || dbLabInstances.data[instanceID]?.use_tunnel, - url: that.state.url || dbLabInstances.data[instanceID]?.url, - sshServerUrl: - that.state.sshServerUrl || - dbLabInstances.data[instanceID]?.ssh_server_url, - }) - } - - if ( - auth && - auth.token && - !projects.isProcessing && - !projects.error && - !projects.isProcessed - ) { - Actions.getProjects(auth.token, orgId) - } - - if ( - auth && - auth.token && - !dbLabInstances?.isProcessing && - !dbLabInstances?.error && - !dbLabInstances?.isProcessed - ) { - Actions.getDbLabInstances(auth.token, orgId, 0) - } - }) - - Actions.refresh() - } - - componentWillUnmount() { - this.unsubscribe() - Actions.resetNewDbLabInstance() - } - - buttonHandler = () => { - const orgId = this.props.orgId ? this.props.orgId : null - const auth = - this.state.data && this.state.data.auth ? this.state.data.auth : null - const data = this.state.data ? this.state.data.newDbLabInstance : null - const errorFields = [] - - if (!this.state.url) { - errorFields.push('url') - } - - if (!this.state.project) { - errorFields.push('project') - } - - if (!this.state.token) { - errorFields.push('token') - } - - if (errorFields.length > 0) { - this.setState({ errorFields: errorFields }) - return - } - - this.setState({ errorFields: [] }) - - if ( - auth && - data && - !data.isUpdating && - this.state.url && - this.state.token && - this.state.project - ) { - Actions[`${this.props.edit ? 'edit' : 'add'}DbLabInstance`](auth.token, { - orgId: orgId, - project: this.state.project, - instanceId: this.props.edit ? this.state.instanceID : null, - projectLabel: this.state.project_label, - url: this.state.url, - instanceToken: this.state.token, - useTunnel: this.state.useTunnel, - sshServerUrl: this.state.sshServerUrl, - }) - } - } - - clearFieldError = (fieldName: string) => { - const errorFields = this.state.errorFields.filter((field) => { - return field !== fieldName - }) - - this.setState({ errorFields: errorFields }) - } - - checkUrlHandler = () => { - const auth = - this.state.data && this.state.data.auth ? this.state.data.auth : null - const data = this.state.data ? this.state.data.newDbLabInstance : null - const errorFields: string[] = [] - - if (!this.state.url) { - errorFields.push('url') - } - - if (!this.state.token) { - errorFields.push('token') - } - - if (errorFields.length > 0) { - this.setState({ errorFields: errorFields }) - return - } - - if (auth && data && !data.isChecking && this.state.url) { - Actions.checkDbLabInstanceUrl( - auth.token, - this.state.url, - this.state.token, - this.state.useTunnel, - ) - } - } - - returnHandler = () => { - this.props.history.push(Urls.linkDbLabInstances(this.props)) - } - - processedHandler = () => { - const data = this.state.data ? this.state.data.newDbLabInstance : null - - this.props.history.push( - Urls.linkDbLabInstance(this.props, data?.data?.id as string), - ) - } - - generateTokenHandler = () => { - this.setState({ token: generateToken() }) - this.clearFieldError('token') - } - - render() { - const { classes, orgPermissions } = this.props - const data = - this.state && this.state.data ? this.state.data.newDbLabInstance : null - const projects = - this.state && this.state.data && this.state.data.projects - ? this.state.data.projects - : null - const projectsList = [] - const dbLabInstances = - this.state && this.state.data && this.state.data.dbLabInstances - ? this.state.data.dbLabInstances - : null - - if (data && data.isProcessed && !data.error) { - this.processedHandler() - Actions.resetNewDbLabInstance() - } - - const breadcrumbs = ( - - ) - - const pageTitle = ( - - ) - - const permitted = !orgPermissions || orgPermissions.dblabInstanceCreate - const disabledOnEdit = this.props.edit - const instancesLoaded = dbLabInstances && dbLabInstances.data - - if (!projects || !projects.data || !instancesLoaded) { - return ( -
- {breadcrumbs} - - {pageTitle} - - -
- ) - } - - if (projects.data && projects.data?.length > 0) { - projects.data.map((p: { name: string; id: number }) => { - return projectsList.push({ title: p.name, value: p.id }) - }) - } - - const isDataUpdating = data && (data.isUpdating || data.isChecking) - - return ( -
- {breadcrumbs} - - {pageTitle} - - {!permitted && ( - - You do not have permission to {this.props.edit ? 'edit' : 'add'}{' '} - Database Lab instances. - - )} - - {!disabledOnEdit && ( - - Database Lab provisioning is currently semi-automated. -
- First, you need to prepare a Database Lab instance on a - separate  machine. Once the instance is ready, register it - here. -
- )} - - -
- { - this.setState({ - project: e.target.value, - }) - this.clearFieldError('project') - Actions.resetNewDbLabInstance() - }} - margin="normal" - error={this.state.errorFields.indexOf('project') !== -1} - fullWidth - inputProps={{ - name: 'project', - id: 'project', - shrink: true, - }} - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
- { - this.setState({ - project_label: e.target.value, - }) - this.clearFieldError('project_label') - Actions.resetNewDbLabInstance() - }} - margin="normal" - error={this.state.errorFields.indexOf('project_label') !== -1} - fullWidth - inputProps={{ - name: 'project_label', - id: 'project_label', - shrink: true, - }} - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- - {!disabledOnEdit && ( -
- { - this.setState({ - token: e.target.value, - }) - this.clearFieldError('token') - Actions.resetNewDbLabInstance() - }} - margin="normal" - error={this.state.errorFields.indexOf('token') !== -1} - fullWidth - inputProps={{ - name: 'token', - id: 'token', - shrink: true, - }} - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
-
- )} - -
- { - this.setState({ - url: e.target.value, - }) - this.clearFieldError('url') - Actions.resetNewDbLabInstance() - }} - margin="normal" - helperText={ - this.state.url && - !isHttps(this.state.url) && - !this.state.useTunnel ? ( - - - - The connection to the Database Lab API is not secure. Use - HTTPS. - - - ) : null - } - error={this.state.errorFields.indexOf('url') !== -1} - fullWidth - inputProps={{ - name: 'url', - id: 'url', - shrink: true, - }} - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
- { - this.setState({ - useTunnel: e.target.checked, - }) - Actions.resetNewDbLabInstance() - }} - id="useTunnel" - name="useTunnel" - /> - } - label="Use tunnel" - labelPlacement="end" - /> -
- { - this.setState({ - sshServerUrl: e.target.value, - }) - this.clearFieldError('token') - Actions.resetNewDbLabInstance() - }} - margin="normal" - error={this.state.errorFields.indexOf('sshServerUrl') !== -1} - fullWidth - inputProps={{ - name: 'sshServerUrl', - id: 'sshServerUrl', - shrink: true, - }} - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
-
-
- - -
- {data?.isCheckProcessed && - data?.isChecked && - (isHttps(this.state.url) || this.state.useTunnel) ? ( - - {' '} - Verified - - ) : null} - - {data?.isCheckProcessed && - data?.isChecked && - !isHttps(this.state.url) && - !this.state.useTunnel ? ( - - Verified but is - not secure - - ) : null} - - {data?.isCheckProcessed && !data?.isChecked ? ( - - Not available - - ) : null} -
-
- -
- -    - -
-
- {data?.errorMessage ? data.errorMessage : null} -
-
-
- ) - } -} - -export default DbLabInstanceForm diff --git a/ui/packages/platform/src/components/AddMemberForm/AddMemberForm.tsx b/ui/packages/platform/src/components/AddMemberForm/AddMemberForm.tsx deleted file mode 100644 index f01ff018..00000000 --- a/ui/packages/platform/src/components/AddMemberForm/AddMemberForm.tsx +++ /dev/null @@ -1,238 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component } from 'react' -import Button from '@material-ui/core/Button' -import Grid from '@material-ui/core/Grid' -import TextField from '@material-ui/core/TextField' - -import { styles } from '@postgres.ai/shared/styles/styles' -import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner' -import { ClassesType, RefluxTypes } from '@postgres.ai/platform/src/components/types' - -import Actions from '../../actions/actions' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import ConsolePageTitle from '../ConsolePageTitle' -import { ErrorWrapper } from 'components/Error/ErrorWrapper' -import Store from '../../stores/store' -import { WarningWrapper } from 'components/Warning/WarningWrapper' -import { messages } from '../../assets/messages' -import { InviteFormProps } from 'components/AddMemberForm/AddMemberFormWrapper' -import { theme } from '@postgres.ai/shared/styles/theme' - -interface InviteFormWithStylesProps extends InviteFormProps { - classes: ClassesType -} - -interface InviteFormState { - email: string - data: { - auth: { - token: string - } | null - inviteUser: { - errorMessage: string - updateErrorFields: string[] - isUpdating: boolean - } | null - orgProfile: { - orgId: number - isProcessing: boolean - isProcessed: boolean - error: boolean - } | null - } -} - -class InviteForm extends Component { - unsubscribe: Function - componentDidMount() { - const that = this - const { org, orgId } = this.props - - this.unsubscribe = (Store.listen as RefluxTypes["listen"]) (function () { - that.setState({ data: this.data }) - - if (this.data.inviteUser.isProcessed && !this.data.inviteUser.error) { - window.location.href = '/' + org + '/members' - } - - const auth: InviteFormState['data']['auth'] = - this.data && this.data.auth ? this.data.auth : null - const orgProfile: InviteFormState['data']['orgProfile'] = - this.data && this.data.orgProfile ? this.data.orgProfile : null - - if ( - auth && - auth.token && - orgProfile && - orgProfile.orgId !== orgId && - !orgProfile.isProcessing && - !orgProfile.error - ) { - Actions.getOrgs(auth.token, orgId) - } - }) - - Actions.refresh() - } - - componentWillUnmount() { - this.unsubscribe() - } - - handleChange = (event: React.ChangeEvent) => { - const value = event.target.value - - this.setState({ - email: value, - }) - } - - buttonHandler = () => { - const orgId = this.props.orgId ? this.props.orgId : null - const auth = - this.state.data && this.state.data.auth ? this.state.data.auth : null - const data = this.state.data ? this.state.data.inviteUser : null - - if (auth && data && !data.isUpdating && this.state.email) { - Actions.inviteUser(auth.token, orgId, this.state.email.trim()) - } - } - - render() { - const { classes, orgPermissions } = this.props - - const breadcrumbs = ( - - ) - - const pageTitle = ( - - ) - - if (orgPermissions && !orgPermissions.settingsMemberAdd) { - return ( -
- {breadcrumbs} - - {pageTitle} - - {messages.noPermissionPage} -
- ) - } - - if (this.state && this.state.data && this.state.data.orgProfile?.error) { - return ( -
- {breadcrumbs} - - {pageTitle} - - -
- ) - } - - if ( - !this.state || - !this.state.data || - !(this.state.data.orgProfile && this.state.data.orgProfile.isProcessed) - ) { - return ( -
- {breadcrumbs} - - {pageTitle} - - -
- ) - } - - const inviteData = this.state.data.inviteUser - - return ( -
- {breadcrumbs} - - {pageTitle} - -
- If the person is not registered yet, ask them to register first. -
- -
- {inviteData && inviteData.errorMessage ? ( -
{inviteData.errorMessage}
- ) : null} -
- - - - - - - - - - - - -
- ) - } -} - -export default InviteForm diff --git a/ui/packages/platform/src/components/AddMemberForm/AddMemberFormWrapper.tsx b/ui/packages/platform/src/components/AddMemberForm/AddMemberFormWrapper.tsx deleted file mode 100644 index ea9f3ba5..00000000 --- a/ui/packages/platform/src/components/AddMemberForm/AddMemberFormWrapper.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import { styles } from '@postgres.ai/shared/styles/styles' -import { RouteComponentProps } from 'react-router' -import { OrgPermissions } from 'components/types' -import InviteForm from 'components/AddMemberForm/AddMemberForm' - -export interface InviteFormProps { - org: string | number - orgId: number - history: RouteComponentProps['history'] - project: string | undefined - orgPermissions: OrgPermissions -} - -export const AddMemberFormWrapper = (props: InviteFormProps) => { - const useStyles = makeStyles( - { - container: { - display: 'flex', - flexWrap: 'wrap', - }, - textField: { - ...styles.inputField, - maxWidth: 400, - }, - dense: { - marginTop: 10, - }, - errorMessage: { - color: 'red', - }, - button: { - marginTop: 17, - display: 'inline-block', - marginLeft: 7, - }, - }, - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/AppUpdateBanner/index.tsx b/ui/packages/platform/src/components/AppUpdateBanner/index.tsx deleted file mode 100644 index 05f68684..00000000 --- a/ui/packages/platform/src/components/AppUpdateBanner/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { observer } from 'mobx-react-lite' - -import { icons } from '@postgres.ai/shared/styles/icons' -import { Button } from '@postgres.ai/shared/components/Button' - -import { appStore } from 'stores/app' - -import styles from './styles.module.scss' - -export const AppUpdateBanner = observer(() => { - if (!appStore.isOutdatedVersion) return null - - return ( -
-
- {icons.updateIcon} UI update is available -
- -
- ) -}) diff --git a/ui/packages/platform/src/components/AppUpdateBanner/styles.module.scss b/ui/packages/platform/src/components/AppUpdateBanner/styles.module.scss deleted file mode 100644 index cc51c1d2..00000000 --- a/ui/packages/platform/src/components/AppUpdateBanner/styles.module.scss +++ /dev/null @@ -1,14 +0,0 @@ -.root { - display: flex; - align-items: center; - flex-wrap: wrap; - background-color: #d7eef2; - padding: 4px 14px; - color: #013a44; -} - -.text { - display: flex; - align-items: center; - margin-right: 16px; -} diff --git a/ui/packages/platform/src/components/Audit/Audit.tsx b/ui/packages/platform/src/components/Audit/Audit.tsx deleted file mode 100644 index 692c09ff..00000000 --- a/ui/packages/platform/src/components/Audit/Audit.tsx +++ /dev/null @@ -1,411 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component } from 'react' -import { - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Button, - ExpansionPanel, - ExpansionPanelSummary, - ExpansionPanelDetails, - TextField, -} from '@material-ui/core' -import ExpandMoreIcon from '@material-ui/icons/ExpandMore' - -import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer' -import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner' -import { Spinner } from '@postgres.ai/shared/components/Spinner' -import { ClassesType, RefluxTypes } from '@postgres.ai/platform/src/components/types' - -import Actions from '../../actions/actions' -import ConsolePageTitle from '../ConsolePageTitle' -import { ErrorWrapper } from 'components/Error/ErrorWrapper' -import Store from '../../stores/store' -import { WarningWrapper } from 'components/Warning/WarningWrapper' -import { messages } from '../../assets/messages' -import format from '../../utils/format' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { AuditProps } from 'components/Audit/AuditWrapper' -import { FilteredTableMessage } from 'components/AccessTokens/FilteredTableMessage/FilteredTableMessage' - -const PAGE_SIZE = 20 -const auditTitle = 'Audit log' - -interface AuditWithStylesProps extends AuditProps { - classes: ClassesType -} - -export interface AuditLogData { - id: number - action: string - actor: string - action_data: { - processed_row_count: number - data_before: Record[] - data_after: Record[] - } - created_at: string - table_name: string -} - -interface AuditState { - filterValue: string - data: { - auth: { - token: string - } | null - auditLog: { - isProcessing: boolean - orgId: number - error: boolean - isComplete: boolean - errorCode: number - errorMessage: string - data: AuditLogData[] - } | null - } -} - -class Audit extends Component { - unsubscribe: Function - componentDidMount() { - const that = this - const orgId = this.props.orgId - - this.unsubscribe = (Store.listen as RefluxTypes["listen"]) (function () { - const auth: AuditState['data']['auth'] = - this.data && this.data.auth ? this.data.auth : null - const auditLog: AuditState['data']['auditLog'] = - this.data && this.data.auditLog ? this.data.auditLog : null - - that.setState({ data: this.data }) - - if ( - auth && - auth.token && - auditLog && - !auditLog.isProcessing && - !auditLog.error && - !that.state - ) { - Actions.getAuditLog(auth.token, { - orgId, - limit: PAGE_SIZE, - }) - } - }) - - const contentContainer = document.getElementById('content-container') - if (contentContainer) { - contentContainer.addEventListener('scroll', () => { - if ( - contentContainer !== null && - contentContainer.scrollTop >= - contentContainer.scrollHeight - contentContainer.offsetHeight - ) { - this.showMore() - } - }) - } - - Actions.refresh() - } - - componentWillUnmount() { - this.unsubscribe() - } - - showMore() { - const { orgId } = this.props - const auth = - this.state.data && this.state.data.auth ? this.state.data.auth : null - const logs = this.state && this.state.data ? this.state.data.auditLog : null - let lastId = null - - if (logs && logs?.data && logs.data?.length) { - lastId = logs.data[logs.data.length - 1].id - } - - if (auth && auth.token && !logs?.isProcessing && lastId) { - Actions.getAuditLog(auth.token, { - orgId, - limit: PAGE_SIZE, - lastId, - }) - } - } - - formatAction = (r: AuditLogData) => { - const { classes } = this.props - let acted = r.action - let actor = r.actor - let actorSrc = '' - let rows = 'row' - - if (!actor) { - actor = 'Unknown' - actorSrc = ' (changed directly in database) ' - } - - if (r.action_data && r.action_data.processed_row_count) { - rows = - r.action_data.processed_row_count + - ' ' + - (r.action_data.processed_row_count > 1 ? 'rows' : 'row') - } - - switch (r.action) { - case 'insert': - acted = ' added ' + rows + ' to' - break - case 'delete': - acted = ' deleted ' + rows + ' from' - break - default: - acted = ' updated ' + rows + ' in' - } - - return ( -
- {actor} - {actorSrc} {acted} table {r.table_name} -
- ) - } - - getDataSectionTitle = (r: AuditLogData, before: boolean) => { - switch (r.action) { - case 'insert': - return '' - case 'delete': - return '' - default: - return before ? 'Before:' : 'After:' - } - } - - getChangesTitle = (r: AuditLogData) => { - const displayedCount = r.action_data && r.action_data.data_before - ? r.action_data.data_before?.length - : r.action_data?.data_after?.length - const objCount = - r.action_data && r.action_data.processed_row_count - ? r.action_data.processed_row_count - : null - - if (displayedCount && (objCount as number) > displayedCount) { - return ( - 'Changes (displayed ' + - displayedCount + - ' rows out of ' + - objCount + - ')' - ) - } - - return 'Changes' - } - - filterInputHandler = (event: React.ChangeEvent) => { - this.setState({ filterValue: event.target.value }) - } - - render() { - const { classes, orgPermissions, orgId } = this.props - const data = this.state && this.state.data ? this.state.data.auditLog : null - const logsStore = - (this.state && this.state.data && this.state.data.auditLog) || null - const logs = (logsStore && logsStore.data) || [] - - const breadcrumbs = ( - - ) - - const filteredLogs = logs.filter( - (log) => - log.actor - ?.toLowerCase() - .indexOf((this.state.filterValue || '')?.toLowerCase()) !== -1, - ) - - const pageTitle = ( - - ) - - if (orgPermissions && !orgPermissions.auditLogView) { - return ( -
- {breadcrumbs} - {pageTitle} - {messages.noPermissionPage} -
- ) - } - - if ( - !logsStore || - !logsStore.data || - (logsStore && logsStore.orgId !== orgId) - ) { - return ( -
- {breadcrumbs} - {pageTitle} - -
- ) - } - - if (logsStore.error) { - return ( -
- -
- ) - } - - return ( -
- {breadcrumbs} - {pageTitle} - {filteredLogs && filteredLogs.length > 0 ? ( -
- - - - - Action - Time - - - - {logs.map((r) => { - return ( - - - {this.formatAction(r)} - {((r.action_data && r.action_data.data_before) || (r.action_data && r.action_data.data_after)) && ( -
- - } - aria-controls="panel1b-content" - id="panel1b-header" - className={classes?.expansionPanelSummary} - > - {this.getChangesTitle(r)} - - - {r.action_data && r.action_data.data_before && ( -
- {this.getDataSectionTitle(r, true)} - -
- )} - {r.action_data && r.action_data.data_after && ( -
- {this.getDataSectionTitle(r, false)} - -
- )} -
-
-
- )} -
- - {format.formatTimestampUtc(r.created_at)} - -
- ) - })} -
-
-
-
- {data && data.isProcessing && ( - - )} - {data && !data.isProcessing && !data.isComplete && ( - - )} -
-
- ) : ( - - this.setState({ - filterValue: '', - }) - } - /> - )} -
-
- ) - } -} - -export default Audit diff --git a/ui/packages/platform/src/components/Audit/AuditWrapper.tsx b/ui/packages/platform/src/components/Audit/AuditWrapper.tsx deleted file mode 100644 index 7387d910..00000000 --- a/ui/packages/platform/src/components/Audit/AuditWrapper.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import { styles } from '@postgres.ai/shared/styles/styles' -import { OrgPermissions } from 'components/types' -import Audit from 'components/Audit/Audit' - -export interface AuditProps { - orgId: number - org: string | number - project: string | undefined - orgPermissions: OrgPermissions -} - -export const AuditWrapper = (props: AuditProps) => { - const useStyles = makeStyles( - (theme) => ({ - root: { - ...(styles.root as Object), - display: 'flex', - flexDirection: 'column', - paddingBottom: '20px', - }, - container: { - display: 'flex', - flexWrap: 'wrap', - }, - timeCell: { - verticalAlign: 'top', - minWidth: 200, - }, - expansionPanel: { - boxShadow: 'none', - background: 'transparent', - fontSize: '12px', - marginBottom: '5px', - }, - expansionPanelSummary: { - display: 'inline-block', - padding: '0px', - minHeight: '22px', - '& .MuiExpansionPanelSummary-content': { - margin: '0px', - display: 'inline-block', - }, - '&.Mui-expanded': { - minHeight: '22px', - }, - '& .MuiExpansionPanelSummary-expandIcon': { - display: 'inline-block', - padding: '0px', - }, - }, - expansionPanelDetails: { - padding: '0px', - [theme.breakpoints.down('md')]: { - display: 'block', - }, - }, - actionDescription: { - marginBottom: '5px', - }, - code: { - width: '100%', - 'margin-top': 0, - '& > div': { - paddingTop: 8, - padding: 8, - }, - 'background-color': 'rgb(246, 248, 250)', - '& > div > textarea': { - fontFamily: - '"Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas",' + - ' "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace', - color: 'black', - fontSize: '12px', - }, - }, - showMoreContainer: { - marginTop: 20, - textAlign: 'center', - }, - data: { - width: '50%', - [theme.breakpoints.up('md')]: { - width: '50%', - marginRight: '10px', - }, - }, - bottomSpace: { - ...styles.bottomSpace, - }, - }), - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx deleted file mode 100644 index 5d3538f3..00000000 --- a/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsForm.tsx +++ /dev/null @@ -1,426 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import React, { useEffect, useMemo, useState } from 'react' -import { Link } from '@postgres.ai/shared/components/Link2' -import { - Grid, - Button, - FormControl, - FormControlLabel, - makeStyles, - Typography -} from '@material-ui/core' -import * as Yup from 'yup'; -import Store from '../../stores/store' -import Actions from '../../actions/actions' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import ConsolePageTitle from '../ConsolePageTitle' -import { AuditSettingsFormProps } from './AuditSettingsFormWrapper' -import { styles } from "@postgres.ai/shared/styles/styles"; -import { PageSpinner } from "@postgres.ai/shared/components/PageSpinner"; -import { WarningWrapper } from "../Warning/WarningWrapper"; -import { messages } from "../../assets/messages"; -import { ExternalIcon } from "@postgres.ai/shared/icons/External"; -import { useFormik } from "formik"; -import Checkbox from '@mui/material/Checkbox/Checkbox' -import { SIEMIntegrationForm } from "../SIEMIntegrationForm/SIEMIntegrationForm"; - -type AuditSettingState = { - data: { - auth: { - token: string | null - } | null - orgProfile: { - isUpdating: boolean - error: boolean - updateError: boolean - errorMessage: string | undefined - errorCode: number | undefined - updateErrorMessage: string | null - isProcessing: boolean - orgId: number | null - updateErrorFields: string[] - data: { - siem_integration_enabled: SiemSettings["enableSiemIntegration"] - siem_integration_url: SiemSettings["urlSchema"] - siem_integration_request_headers: SiemSettings["headers"] - audit_events_to_log: string[] - } - } | null - auditEvents: { - isProcessing: boolean - data: { - id: number - event_name: string - label: string - }[] | null - } | null - } | null -} - -interface SiemSettings { - enableSiemIntegration: boolean; - urlSchema?: string; - headers: { key: string; value: string }[]; - auditEvents: EventsToLog[]; -} - -interface EventsToLog { - id: number; - event_name: string; - label: string; -} - -export interface FormValues { - siemSettings: SiemSettings; -} - -const useStyles = makeStyles( - { - container: { - ...(styles.root as Object), - display: 'flex', - 'flex-wrap': 'wrap', - 'min-height': 0, - '&:not(:first-child)': { - 'margin-top': '20px', - }, - }, - formContainer: { - flexWrap: 'nowrap' - }, - textField: { - ...styles.inputField, - }, - instructionsField: { - ...styles.inputField, - }, - selectField: { - marginTop: 4, - - }, - label: { - color: '#000!important', - fontWeight: 'bold', - }, - updateButtonContainer: { - marginTop: 20, - textAlign: 'left', - }, - unlockNote: { - marginTop: 8, - '& ol': { - paddingLeft: 24, - marginTop: 6, - marginBottom: 0 - } - }, - externalIcon: { - width: 14, - height: 14, - marginLeft: 4, - transform: 'translateY(2px)', - }, - testConnectionButton: { - marginRight: 16 - }, - eventRow: { - display: 'flex', - alignItems: 'center', - marginBottom: '10px', - }, - }, - { index: 1 }, -) - -const validationSchema = Yup.object({ - siemSettings: Yup.object({ - urlSchema: Yup.string() - .url('Invalid URL format') // Validates that the input is a valid URL - .required('URL is required'), // Field is mandatory - headers: Yup.array().of( - Yup.object({ - key: Yup.string().optional(), - value: Yup.string().optional(), - }) - ), - auditEvents: Yup.array() - }), -}); - -const AuditSettingsForm: React.FC = (props) => { - const { orgPermissions, orgData, orgId, org, project } = props; - const classes = useStyles(); - const [data, setData] = useState(null); - - useEffect(() => { - const unsubscribe = Store.listen(function () { - const newStoreData = this.data; - - if (JSON.stringify(newStoreData) !== JSON.stringify(data)) { - const auth = newStoreData?.auth || null; - const orgProfile = newStoreData?.orgProfile || null; - const auditEvents = newStoreData?.auditEvents || null; - - if ( - auth?.token && - orgProfile && - orgProfile.orgId !== orgId && - !orgProfile.isProcessing - ) { - Actions.getOrgs(auth.token, orgId); - } - - if (auth?.token && auditEvents && !auditEvents.isProcessing) { - Actions.getAuditEvents(auth.token); - } - - setData(newStoreData); - } - }); - - Actions.refresh(); - - return () => { - unsubscribe(); - }; - }, [orgId, data, props.match.params.projectId]); - - const isAuditLogsSettingsAvailable = useMemo(() => { - const privileged_until = orgData?.priveleged_until; - return !!(orgData && privileged_until && new Date(privileged_until) > new Date() && orgData?.data?.plan === 'EE'); - - }, [orgData]) - - const formik = useFormik({ - enableReinitialize: true, - initialValues: { - siemSettings: { - enableSiemIntegration: Boolean(data?.orgProfile?.data?.siem_integration_enabled), - urlSchema: data?.orgProfile?.data?.siem_integration_url || '', - headers: data?.orgProfile?.data?.siem_integration_request_headers - ? Object.entries(data.orgProfile.data.siem_integration_request_headers).map(([key, value]) => ({ - key: key || '', - value: value || '', - })) as unknown as SiemSettings['headers'] - : [{ key: '', value: '' }], - auditEvents: data?.auditEvents?.data - ? data?.auditEvents?.data - ?.filter((event) => - data?.orgProfile?.data?.audit_events_to_log?.includes(event.event_name) - ) - ?.map((event) => ({ - id: event.id, - event_name: event.event_name, - label: event.label, - })) - : [], - }, - }, - validationSchema, - onSubmit: async (values, { setSubmitting }) => { - const errors = await formik.validateForm(); - - if (Object.keys(errors).length > 0) { - console.error('Validation errors:', errors); - setSubmitting(false); - return; // Stop submission if there are errors - } - - const currentOrgId = orgId || null; - const auth = data?.auth || null; - - if (auth) { - const params = formik.values.siemSettings; - try { - await Actions.updateAuditSettings(auth.token, currentOrgId, params); - } catch (error) { - const errorMessage = `Error updating audit settings: ${error}`; - Actions.showNotification(errorMessage, 'error'); - console.error('Error updating audit settings:', error); - } finally { - setSubmitting(false); - } - } - } - }); - - const isDisabled = useMemo(() => - !isAuditLogsSettingsAvailable || !formik.values.siemSettings.enableSiemIntegration, - [isAuditLogsSettingsAvailable, formik.values.siemSettings.enableSiemIntegration] - ); - - const testConnection = async () => { - try { - const auth = data?.auth || null; - - if (auth) { - const params = {...formik.values.siemSettings}; - if (formik.values.siemSettings.urlSchema) { - Actions.testSiemServiceConnection(auth.token, params); - } - } - } catch (error) { - console.error('Connection failed:', error); - } - }; - - const breadcrumbs = ( - - ); - - const pageTitle = ; - - if (orgPermissions && !orgPermissions.settingsOrganizationUpdate) { - return ( - <> - {breadcrumbs} - {pageTitle} - {messages.noPermissionPage} - - ); - } - - if (!data || (data && data.orgProfile && data.orgProfile.isProcessing) || (data && data.auditEvents && data.auditEvents.isProcessing)) { - return ( -
- {breadcrumbs} - {pageTitle} - -
- ); - } - - return ( - <> - {breadcrumbs} - {pageTitle} -
- - - - {!isAuditLogsSettingsAvailable && - - Become an Enterprise customer - - -  to unlock audit settings - } - - - SIEM audit logs integration documentation - - - - - -

SIEM integration

- - formik.setFieldValue( - 'siemSettings.enableSiemIntegration', - e.target.checked - ) - } - /> - } - label="Send audit events to SIEM system" - disabled={!isAuditLogsSettingsAvailable} - /> -

SIEM connection settings

- -
-
-
- - - - - - -

Select audit events to export

- {data?.auditEvents?.data && - data?.auditEvents?.data?.map((event) => { - const isChecked = formik.values.siemSettings.auditEvents.some( - (e) => e.event_name === event.event_name - ); - - return ( -
- { - const updatedAuditEvents = e.target.checked - ? [...formik.values.siemSettings.auditEvents, { ...event }] - : formik.values.siemSettings.auditEvents.filter( - (auditEvent) => auditEvent.event_name !== event.event_name - ); - - formik.setFieldValue('siemSettings.auditEvents', updatedAuditEvents); - }} - /> - } - label={event.label} - disabled={isDisabled} - /> -
- ); - })} -
-
-
- - - -
-
-
- - ); -}; - -export default AuditSettingsForm diff --git a/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsFormWrapper.tsx b/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsFormWrapper.tsx deleted file mode 100644 index 3ae26ec9..00000000 --- a/ui/packages/platform/src/components/AuditSettingsForm/AuditSettingsFormWrapper.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; -import AuditSettingsForm from "./AuditSettingsForm"; - -export interface AuditSettingsFormProps { - mode?: string | undefined - project?: string | undefined - org?: string | number - orgId?: number - orgPermissions?: { - settingsOrganizationUpdate?: boolean - } - orgData?: { - priveleged_until: Date - chats_private_allowed: boolean - data?: { - plan?: string - } | null - } - match: { - params: { - project?: string - projectId?: string | number | undefined - org?: string - } - } -} - - - -export const AuditSettingsFormWrapper = (props: AuditSettingsFormProps) => { - return -} diff --git a/ui/packages/platform/src/components/Billing/Billing.tsx b/ui/packages/platform/src/components/Billing/Billing.tsx deleted file mode 100644 index a6eb24ed..00000000 --- a/ui/packages/platform/src/components/Billing/Billing.tsx +++ /dev/null @@ -1,180 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component } from 'react' -import { loadStripe } from '@stripe/stripe-js' -import { Elements } from '@stripe/react-stripe-js' -import { ClassesType, RefluxTypes } from '@postgres.ai/platform/src/components/types' - -import ConsolePageTitle from '../ConsolePageTitle' -import StripeForm from '../StripeForm' -import settings from '../../utils/settings' -import Store from '../../stores/store' -import Actions from '../../actions/actions' -import Permissions from '../../utils/permissions' -import { ErrorWrapper } from 'components/Error/ErrorWrapper' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { BillingProps } from 'components/Billing/BillingWrapper' - -interface BillingWithStylesProps extends BillingProps { - classes: ClassesType -} - -interface BillingState { - data: { - auth: { - token: string - } | null - billing: { - orgId: number - error: boolean - isProcessing: boolean - subscriptionError: boolean - subscriptionErrorMessage: string - isSubscriptionProcessing: boolean - primaryPaymentMethod: string - data: { - unit_amount: string - data_usage_estimate: string - data_usage_sum: string - data_usage: { - id: number - instance_id: string - day_date: Date - data_size_gib: number - to_invoice: boolean - }[] - period_start: Date - period_now: Date - } - } - } -} - -const stripePromise = loadStripe(settings.stripeApiKey as string, { - locale: 'en', -}) - -const page = { - title: 'Billing', -} - -class Billing extends Component { - unsubscribe: Function - componentDidMount() { - const that = this - const { orgId } = this.props - - this.unsubscribe = (Store.listen as RefluxTypes["listen"]) (function () { - const auth: BillingState['data']['auth'] = - this.data && this.data.auth ? this.data.auth : null - const billing: BillingState['data']['billing'] = - this.data && this.data.billing ? this.data.billing : null - - that.setState({ data: this.data }) - - if ( - auth && - auth.token && - billing && - !billing.isProcessing && - !billing.error && - !that.state - ) { - Actions.getBillingDataUsage(auth.token, orgId) - } - }) - - Actions.refresh() - } - - componentWillUnmount() { - this.unsubscribe() - } - - toFixed(value: number) { - if (value && value.toFixed && value !== 0) { - return value.toFixed(4) - } - - return '0.0' - } - - render() { - const { classes, orgId, orgData } = this.props - const auth = - this.state && this.state.data && this.state.data.auth - ? this.state.data.auth - : null - const data = - this.state && this.state.data && this.state.data.billing - ? this.state.data.billing - : null - - const breadcrumbs = ( - - ) - - if (!Permissions.isAdmin(orgData)) { - return ( -
- {breadcrumbs} - - {} - - -
- ) - } - - let mode = 'new' - if (orgData.is_blocked && orgData.stripe_subscription_id) { - mode = 'resume' - } - if (!orgData.is_blocked && orgData.stripe_subscription_id) { - mode = 'update' - } - - return ( -
- {breadcrumbs} - -
-
- {Permissions.isAdmin(orgData) && ( -
- {data && data.subscriptionError && ( -
- {data.subscriptionErrorMessage} -
- )} - - - -
- )} -
-
-
-
- ) - } -} - -export default Billing diff --git a/ui/packages/platform/src/components/Billing/BillingWrapper.tsx b/ui/packages/platform/src/components/Billing/BillingWrapper.tsx deleted file mode 100644 index 6c4497fe..00000000 --- a/ui/packages/platform/src/components/Billing/BillingWrapper.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import Billing from 'components/Billing/Billing' -import { colors } from '@postgres.ai/shared/styles/colors' - -export interface BillingProps { - org: string | number - orgId: number - short: boolean - projectId: number | string | undefined - orgData: { - alias: string - is_priveleged: boolean - stripe_payment_method_primary: string - is_blocked: boolean - new_subscription: boolean - is_blocked_on_creation: boolean - stripe_subscription_id: number - priveleged_until: Date - role: { - id: number - } - } -} - -export const BillingWrapper = (props: BillingProps) => { - const useStyles = makeStyles( - (theme) => ({ - root: { - '& ul': { - '& > li': { - 'list-style-position': 'inside', - }, - padding: 0, - }, - '& h1': { - fontSize: '16px!important', - fontWeight: 'bold', - }, - '& h2': { - fontSize: '14px!important', - fontWeight: 'bold', - }, - width: '100%', - 'min-height': '100%', - 'z-index': 1, - position: 'relative', - [theme.breakpoints.down('sm')]: { - maxWidth: '100vw', - }, - [theme.breakpoints.up('md')]: { - maxWidth: 'calc(100vw - 200px)', - }, - [theme.breakpoints.up('lg')]: { - maxWidth: 'calc(100vw - 200px)', - }, - 'font-size': '14px!important', - 'font-family': '"Roboto", "Helvetica", "Arial", sans-serif', - - display: 'flex', - flexDirection: 'column', - paddingBottom: '20px', - }, - errorMessage: { - color: colors.state.error, - marginBottom: 10, - }, - subscriptionForm: { - marginBottom: 20, - }, - }), - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx b/ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx deleted file mode 100644 index ddab1e03..00000000 --- a/ui/packages/platform/src/components/BotSettingsForm/BotSettingsForm.tsx +++ /dev/null @@ -1,303 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import React, { useEffect, useMemo, useState } from 'react' -import { Link } from '@postgres.ai/shared/components/Link2' -import { - Grid, - Button, - InputLabel, - FormControl, - FormControlLabel, - makeStyles, - Typography -} from '@material-ui/core' -import Store from '../../stores/store' -import Actions from '../../actions/actions' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import ConsolePageTitle from '../ConsolePageTitle' -import { BotSettingsFormProps } from './BotSettingsFormWrapper' -import { styles } from "@postgres.ai/shared/styles/styles"; -import { PageSpinner } from "@postgres.ai/shared/components/PageSpinner"; -import { WarningWrapper } from "../Warning/WarningWrapper"; -import { messages } from "../../assets/messages"; -import RadioGroup from '@mui/material/RadioGroup' -import Radio from '@mui/material/Radio' -import { ExternalIcon } from "@postgres.ai/shared/icons/External"; -import { useFormik } from "formik"; - -type DbLabInstance = { - id: number; - plan: string | null; -} - -type BotSettingState = { - data: { - auth: { - token: string | null - } | null - orgProfile: { - isUpdating: boolean - error: boolean - updateError: boolean - errorMessage: string | undefined - errorCode: number | undefined - updateErrorMessage: string | null - isProcessing: boolean - orgId: number | null - updateErrorFields: string[] - data: { - is_chat_public_by_default: boolean - } - } | null - dbLabInstances: { - data: Record; - } - } | null -} - -const useStyles = makeStyles( - { - container: { - ...(styles.root as Object), - display: 'flex', - 'flex-wrap': 'wrap', - 'min-height': 0, - '&:not(:first-child)': { - 'margin-top': '20px', - }, - }, - textField: { - ...styles.inputField, - }, - instructionsField: { - ...styles.inputField, - }, - selectField: { - marginTop: 4, - '& .MuiInputLabel-formControl': { - transform: 'none', - position: 'static' - } - }, - label: { - color: '#000!important', - fontWeight: 'bold', - }, - radioGroup: { - marginTop: 8 - }, - updateButtonContainer: { - marginTop: 20, - textAlign: 'left', - }, - errorMessage: { - color: 'red', - }, - unlockNote: { - marginTop: 8, - '& ol': { - paddingLeft: 24, - marginTop: 6, - marginBottom: 0 - } - }, - formControlLabel: { - '& .Mui-disabled > *, & .Mui-disabled': { - color: 'rgba(0, 0, 0, 0.6)' - } - }, - externalIcon: { - width: 14, - height: 14, - marginLeft: 4, - transform: 'translateY(2px)', - } - }, - { index: 1 }, -) - -const BotSettingsForm: React.FC = (props) => { - const { orgPermissions, orgData, orgId, org, project } = props; - - const classes = useStyles() - - const [data, setData] = useState(null) - - - useEffect(() => { - const unsubscribe = Store.listen(function () { - const newStoreData = this.data; - - if (JSON.stringify(newStoreData) !== JSON.stringify(data)) { - const auth = newStoreData?.auth || null; - const orgProfile = newStoreData?.orgProfile || null; - - if ( - auth?.token && - orgProfile && - orgProfile.orgId !== orgId && - !orgProfile.isProcessing - ) { - Actions.getOrgs(auth.token, orgId); - } - - setData(newStoreData); - } - }); - - Actions.refresh(); - - return () => { - unsubscribe(); - }; - }, [orgId, data, props.match.params.projectId]); - - const formik = useFormik({ - enableReinitialize: true, - initialValues: { - threadVisibility: - data?.orgProfile?.data?.is_chat_public_by_default ? 'public' : 'private' - }, - onSubmit: () => { - const currentOrgId = orgId || null; - const auth = data?.auth || null; - - if (auth) { - let params: { is_chat_public_by_default?: boolean } = { - is_chat_public_by_default: - formik.values.threadVisibility === 'public', - }; - - Actions.updateAiBotSettings(auth.token, currentOrgId, params); - } - }, - }); - - const handleChangeThreadVisibility = ( - event: React.ChangeEvent<{ value: string }> - ) => { - formik.handleChange(event); - }; - - const breadcrumbs = ( - - ) - - const pageTitle = ( - - ) - - if (orgPermissions && !orgPermissions.settingsOrganizationUpdate) { - return ( - <> - {breadcrumbs} - - {pageTitle} - - {messages.noPermissionPage} - - ) - } - - if (!data || (data && data.orgProfile && data.orgProfile.isProcessing)) { - return ( -
- {breadcrumbs} - - {pageTitle} - - -
- ) - } - - return ( - <> - {breadcrumbs} - - {pageTitle} -
- - - - - - - AI chats default visibility - - - } - label={<>Public: anyone can view chats, but only team members can respond} - /> - } - label={<>Private: chats are visible only to members of your organization} - /> - {!orgData?.chats_private_allowed && - Unlock private conversations by either: -
    -
  1. - - Installing a DBLab SE instance - - -
  2. -
  3. - - Becoming a Postgres.AI consulting customer - - -
  4. -
-
} -
-
-
-
- - - -
-
-
- - ) -} - -export default BotSettingsForm diff --git a/ui/packages/platform/src/components/BotSettingsForm/BotSettingsFormWrapper.tsx b/ui/packages/platform/src/components/BotSettingsForm/BotSettingsFormWrapper.tsx deleted file mode 100644 index c800860e..00000000 --- a/ui/packages/platform/src/components/BotSettingsForm/BotSettingsFormWrapper.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; -import BotSettingsForm from "./BotSettingsForm"; - -export interface BotSettingsFormProps { - mode?: string | undefined - project?: string | undefined - org?: string | number - orgId?: number - orgPermissions?: { - settingsOrganizationUpdate?: boolean - } - orgData?: { - priveleged_until: Date - chats_private_allowed: boolean - data?: { - plan?: string - } | null - } - match: { - params: { - project?: string - projectId?: string | number | undefined - org?: string - } - } -} - - - -export const BotSettingsFormWrapper = (props: BotSettingsFormProps) => { - return -} diff --git a/ui/packages/platform/src/components/CheckupAgentForm/CheckupAgentForm.tsx b/ui/packages/platform/src/components/CheckupAgentForm/CheckupAgentForm.tsx deleted file mode 100644 index 98d568a8..00000000 --- a/ui/packages/platform/src/components/CheckupAgentForm/CheckupAgentForm.tsx +++ /dev/null @@ -1,1031 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component } from 'react' -import { - Typography, - IconButton, - TextField, - Chip, - Grid, - Tabs, - Tab, - Button, - Radio, - RadioGroup, - FormControlLabel, - FormLabel, - ExpansionPanel, - ExpansionPanelSummary, - ExpansionPanelDetails, -} from '@material-ui/core' -import Box from '@mui/material/Box' -import ExpandMoreIcon from '@material-ui/icons/ExpandMore' - -import { styles } from '@postgres.ai/shared/styles/styles' -import { icons } from '@postgres.ai/shared/styles/icons' -import { Spinner } from '@postgres.ai/shared/components/Spinner' -import { - ClassesType, - TabPanelProps, - ProjectProps, - TokenRequestProps, - RefluxTypes, -} from '@postgres.ai/platform/src/components/types' - -import Store from '../../stores/store' -import Actions from '../../actions/actions' -import { ErrorWrapper } from 'components/Error/ErrorWrapper' -import CfgGen, { DataType } from '../../utils/cfggen' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { CheckupAgentFormProps } from 'components/CheckupAgentForm/CheckupAgentFormWrapper' - -const AUTO_GENERATED_TOKEN_NAME = 'Auto-generated 1-year token' - -interface CheckupAgentFormWithStylesProps extends CheckupAgentFormProps { - classes: ClassesType -} - -interface CheckupAgentFormState extends DataType { - tab: number - data: { - auth: { - token: string - } | null - tokenRequest: TokenRequestProps - reports: { - error: boolean - isProcessed: boolean - isProcessing: boolean - } | null - projects: Omit - } -} - -function TabPanel(props: TabPanelProps) { - const { children, value, index, ...other } = props - - return ( - - ) -} - -function a11yProps(index: number) { - return { - id: `simple-tab-${index}`, - 'aria-controls': `simple-tabpanel-${index}`, - } -} - -class CheckupAgentForm extends Component< - CheckupAgentFormWithStylesProps, - CheckupAgentFormState -> { - state = { - data: { - auth: { - token: '', - }, - tokenRequest: { - isProcessing: false, - isProcessed: false, - data: { - name: '', - is_personal: false, - expires_at: '', - token: '', - }, - errorMessage: '', - error: false, - }, - reports: { - error: false, - isProcessed: false, - isProcessing: false, - }, - projects: { - error: false, - isProcessing: false, - isProcessed: false, - }, - }, - hosts: '', - projectName: '', - databaseName: '', - databaseUserName: '', - ssDatabaseName: '', - port: null, - sshPort: null, - pgPort: null, - statementTimeout: null, - pgSocketDir: '', - psqlBinary: '', - collectPeriod: 600, - newHostName: '', - apiToken: '', - sshKeysPath: '', - password: '', - connectionType: '', - tab: 0, - } - - unsubscribe: Function - componentDidMount() { - const that = this - - this.unsubscribe = (Store.listen as RefluxTypes["listen"]) (function () { - const auth: CheckupAgentFormState['data']['auth'] = - this.data && this.data.auth ? this.data.auth : null - const reports: CheckupAgentFormState['data']['reports'] = - this.data && this.data.reports ? this.data.reports : null - const projects: Omit = - this.data && this.data.projects ? this.data.projects : null - const tokenRequest: TokenRequestProps = - this.data && this.data.tokenRequest ? this.data.tokenRequest : null - - that.setState({ data: this.data }) - - if ( - auth && - auth.token && - !reports?.isProcessed && - !reports?.isProcessing && - !reports?.error - ) { - Actions.getCheckupReports(auth.token) - } - - if ( - auth && - auth.token && - !projects?.isProcessed && - !projects?.isProcessing && - !projects?.error - ) { - Actions.getProjects(auth.token) - } - - if ( - tokenRequest && - tokenRequest.isProcessed && - !tokenRequest.error && - tokenRequest.data && - tokenRequest.data.name && - tokenRequest.data.name.startsWith(AUTO_GENERATED_TOKEN_NAME) && - tokenRequest.data.expires_at && - tokenRequest.data.token - ) { - that.setState({ apiToken: tokenRequest.data.token }) - } - }) - - Actions.refresh() - CfgGen.generateRunCheckupSh(this.state) - } - - componentWillUnmount() { - Actions.hideGeneratedAccessToken() - this.unsubscribe() - } - - handleDeleteHost = (_: React.ChangeEvent, host: string) => { - const curHosts = CfgGen.uniqueHosts(this.state.hosts) - const curDividers = this.state.hosts.match(/[;,(\s)(\n)(\r)(\t)(\r\n)]/gm) - const hosts = curHosts.split(';') - let newHosts = '' - - for (const i in hosts) { - if (hosts[i] !== host) { - newHosts = - newHosts + - hosts[i] + - (curDividers !== null && curDividers[i] ? curDividers[i] : '') - } - } - - this.setState({ hosts: newHosts }) - } - - handleChangeTab = (_: React.ChangeEvent<{}>, tabValue: number) => { - this.setState({ tab: tabValue }) - } - - addToken = () => { - const orgId = this.props.orgId ? this.props.orgId : null - const auth = - this.state.data && this.state.data.auth ? this.state.data.auth : null - const tokenRequest = - this.state.data && this.state.data.tokenRequest - ? this.state.data.tokenRequest - : null - - if (auth && auth.token && !tokenRequest?.isProcessing) { - const date = new Date() - const expiresAt = - date.getFullYear() + - 1 + - '-' + - ('0' + (date.getMonth() + 1)).slice(-2) + - '-' + - ('0' + date.getDate()).slice(-2) - const nowDateTime = - date.getFullYear() + - '-' + - ('0' + (date.getMonth() + 1)).slice(-2) + - '-' + - ('0' + date.getDate()).slice(-2) + - ' ' + - ('0' + date.getHours()).slice(-2) + - ':' + - ('0' + date.getMinutes()).slice(-2) - const tokenName = AUTO_GENERATED_TOKEN_NAME + ' (' + nowDateTime + ')' - - Actions.getAccessToken(auth.token, tokenName, expiresAt, orgId) - } - } - - copyDockerCfg = () => { - const copyText = document.getElementById( - 'generatedDockerCfg', - ) as HTMLInputElement - - if (copyText) { - copyText.select() - copyText.setSelectionRange(0, 99999) - document.execCommand('copy') - copyText.setSelectionRange(0, 0) - } - } - - copySrcCfg = () => { - const copyText = document.getElementById( - 'generatedSrcCfg', - ) as HTMLInputElement - - if (copyText) { - copyText.select() - copyText.setSelectionRange(0, 99999) - document.execCommand('copy') - copyText.setSelectionRange(0, 0) - } - } - - render() { - const that = this - const { classes } = this.props - const reports = - this.state.data && this.state.data.reports - ? this.state.data.reports - : null - const projects = - this.state.data && this.state.data.projects - ? this.state.data.projects - : null - const tokenRequest = - this.state.data && this.state.data.tokenRequest - ? this.state.data.tokenRequest - : null - let copySrcCfgBtn = null - let copyDockerCfgBtn = null - let token = null - let content = null - - if ( - this.state.projectName !== '' && - this.state.databaseName !== '' && - this.state.databaseUserName !== '' && - this.state.hosts !== '' && - this.state.apiToken !== '' - ) { - copySrcCfgBtn = ( - - {icons.copyIcon} - - ) - copyDockerCfgBtn = ( - - {icons.copyIcon} - - ) - } - - token = ( -
- { - this.setState({ - apiToken: e.target.value, - }) - }} - value={this.state.apiToken} - helperText={ - 'Insert a token or generate a new one. ' + - 'The auto-generated token will expire in 1 year.' - } - inputProps={{ - name: 'apiToken', - id: 'apiToken', - }} - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> - - -
- ) - - if ( - this.state && - this.state.data && - ((this.state.data.reports && this.state.data.reports.error) || - (this.state.data.projects && this.state.data.projects.error)) - ) { - return ( -
- -
- ) - } - - if (reports && reports.isProcessed && projects?.isProcessed) { - content = ( -
- - Use postgres-checkup to check health of your Postgres databases. - This page will help you to generate the configuration file. Provide - settings that you will use inside your private network (local - hostnames, IPs, etc). - -
- - - Do not leave the page in order not to loose the configuration data. - -
- -

1. Configure

- - - - - General options - - - - - -
- { - this.setState({ - projectName: e.target.value, - }) - }} - required - className={classes.textInput} - value={this.state.projectName} - inputProps={{ - name: 'projectName', - id: 'projectName', - shrink: true, - }} - fullWidth - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
- - Connection type * - - { - this.setState({ - connectionType: e.target.value, - }) - }} - className={classes.radioButton} - > - } - label="Connect to defined host via SSH" - /> - } - label={ - 'Connect directly to PostgreSQL (some ' + - 'reports won’t be available)' - } - /> - -
- -
-
-
- { - this.setState({ - hosts: e.target.value, - }) - }} - value={this.state.hosts} - multiline - label="Hosts" - fullWidth - required - inputProps={{ - name: 'hosts', - id: 'hosts', - }} - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
- {CfgGen.uniqueHosts(that.state.hosts) - .split(';') - .map((h) => { - if (h !== '') { - return ( - - this.handleDeleteHost(event, h) - } - color="primary" - /> - ) - } - - return null - })} -
-
-
- -
- { - this.setState({ - databaseUserName: e.target.value, - }) - }} - required - value={this.state.databaseUserName} - label="Database username" - inputProps={{ - name: 'databaseUserName', - id: 'databaseUserName', - }} - fullWidth - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
- - Database password * - - { - this.setState({ - password: e.target.value, - }) - }} - className={classes.radioButton} - > - } - label={ - 'No password is required or PGPASSWORD ' + - 'environment variable is predefined' - } - /> - } - label={ - 'I will enter the password manually ' + - '(choose this only for manual testing)' - } - /> - -
- -
- { - this.setState({ - databaseName: e.target.value, - }) - }} - required - value={this.state.databaseName} - inputProps={{ - name: 'databaseName', - id: 'databaseName', - }} - fullWidth - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
- { - this.setState({ - collectPeriod: e.target.value, - }) - }} - value={this.state.collectPeriod} - helperText={ - 'The delay between collection of two ' + - 'statistics snapshots, sec' - } - type="number" - inputProps={{ - name: 'collectPeriod', - id: 'collectPeriod', - }} - fullWidth - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
-
-
-
- - - } - aria-controls="panel1b-content" - id="panel1b-header" - className={classes.advancedExpansionPanelSummary} - > - - Advanced options - - - - - -
- { - this.setState({ - ssDatabaseName: e.target.value, - }) - }} - value={this.state.ssDatabaseName} - helperText={ - 'Database name with enabled "pg_stat_statements"' + - ' extension (for detailed query analysis)' - } - inputProps={{ - name: 'ssDatabaseName', - id: 'ssDatabaseName', - }} - fullWidth - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
- { - this.setState({ - pgPort: e.target.value, - }) - }} - value={this.state.port} - helperText="PostgreSQL database server port (default: 5432)" - type="number" - inputProps={{ - name: 'pgPort', - id: 'pgPort', - }} - fullWidth - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
- { - this.setState({ - sshPort: e.target.value, - }) - }} - value={this.state.port} - helperText="SSH server port (default: 22)" - type="number" - inputProps={{ - name: 'sshPort', - id: 'sshPort', - }} - fullWidth - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
- { - this.setState({ - statementTimeout: e.target.value, - }) - }} - value={this.state.statementTimeout} - helperText={ - 'Statement timeout for all SQL queries ' + - '(default: 30 seconds)' - } - type="number" - inputProps={{ - name: 'statementTimeout', - id: 'statementTimeout', - }} - fullWidth - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
- { - this.setState({ - pgSocketDir: e.target.value, - }) - }} - value={this.state.pgSocketDir} - label="PostgreSQL domain socket directory" - helperText="PostgreSQL domain socket directory (default: psql's default)" - inputProps={{ - name: 'pgSocketDir', - id: 'pgSocketDir', - }} - fullWidth - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
- { - this.setState({ - psqlBinary: e.target.value, - }) - }} - value={this.state.psqlBinary} - helperText='Path to "psql" (default: determined by "$PATH")' - inputProps={{ - name: 'psqlBinary', - id: 'psqlBinary', - }} - fullWidth - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
- -
- { - this.setState({ - sshKeysPath: e.target.value, - }) - }} - value={this.state.sshKeysPath} - helperText="Path to directory with SSH keys" - inputProps={{ - name: 'sshKeysPath', - id: 'sshKeysPath', - }} - fullWidth - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - /> -
-
-
-
-
-

- 2. Generate token to upload postgres-checkup reports to console -

-
{token}
- -

3. Deploy using Docker or building from source

- - - - - - - - - Requirements: bash, coreutils, jq, golang, awk, - sed, pandoc, wkhtmltopdf (see{' '} -
- {' '} - README{' '} - - ). -
- Clone repo: - - git clone https://gitlab.com/postgres-ai/postgres-checkup.git && - cd postgres-checkup - -
- Start script below: - -
- - {copySrcCfgBtn} -
- - - - - Requirements: Docker -
- Start script below: -
-
- - {copyDockerCfgBtn} -
-
-
- ) - } else { - content = ( -
- -
- ) - } - - return ( -
- { - - } - - {content} -
- ) - } -} - -export default CheckupAgentForm diff --git a/ui/packages/platform/src/components/CheckupAgentForm/CheckupAgentFormWrapper.tsx b/ui/packages/platform/src/components/CheckupAgentForm/CheckupAgentFormWrapper.tsx deleted file mode 100644 index 334b13b1..00000000 --- a/ui/packages/platform/src/components/CheckupAgentForm/CheckupAgentFormWrapper.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import { theme } from '@postgres.ai/shared/styles/theme' -import { styles } from '@postgres.ai/shared/styles/styles' -import CheckupAgentForm from 'components/CheckupAgentForm/CheckupAgentForm' - -export interface CheckupAgentFormProps { - orgId: number -} - -export const CheckupAgentFormWrapper = (props: CheckupAgentFormProps) => { - const useStyles = makeStyles( - (muiTheme) => ({ - root: { - 'min-height': '100%', - 'z-index': 1, - position: 'relative', - [muiTheme.breakpoints.down('sm')]: { - maxWidth: '100vw', - }, - [muiTheme.breakpoints.up('md')]: { - maxWidth: 'calc(100vw - 200px)', - }, - [muiTheme.breakpoints.up('lg')]: { - maxWidth: 'calc(100vw - 200px)', - }, - '& h2': { - ...theme.typography.h2, - }, - '& h3': { - ...theme.typography.h3, - }, - '& h4': { - ...theme.typography.h4, - }, - '& .MuiExpansionPanelSummary-root.Mui-expanded': { - minHeight: 24, - }, - }, - heading: { - ...theme.typography.h3, - } as { - [key: string]: string - }, - fieldValue: { - display: 'inline-block', - width: '100%', - }, - tokenInput: { - ...styles.inputField, - margin: 0, - 'margin-top': 10, - 'margin-bottom': 10, - }, - textInput: { - ...styles.inputField, - margin: 0, - marginTop: 0, - marginBottom: 10, - }, - hostItem: { - marginRight: 10, - marginBottom: 5, - marginTop: 5, - }, - fieldRow: { - marginBottom: 10, - display: 'block', - }, - fieldBlock: { - width: '100%', - 'max-width': 600, - 'margin-bottom': 15, - '& > div.MuiFormControl- > label': { - fontSize: 20, - }, - '& input, & .MuiOutlinedInput-multiline': { - padding: 13, - }, - }, - relativeFieldBlock: { - marginBottom: 10, - marginRight: 20, - position: 'relative', - }, - addTokenButton: { - marginLeft: 10, - marginTop: 10, - }, - code: { - width: '100%', - 'margin-top': 0, - '& > div': { - paddingTop: 12, - }, - 'background-color': 'rgb(246, 248, 250)', - '& > div > textarea': { - fontFamily: - '"Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas",' + - ' "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace', - color: 'black', - fontSize: 14, - }, - }, - codeBlock: { - fontFamily: - '"Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas",' + - ' "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace', - width: '100%', - padding: 3, - marginTop: 0, - border: 'rgb(204, 204, 204);', - borderRadius: 3, - color: 'black', - backgroundColor: 'rgb(246, 248, 250)', - }, - details: { - display: 'block', - }, - copyButton: { - position: 'absolute', - top: 6, - right: 6, - fontSize: 20, - }, - relativeDiv: { - position: 'relative', - }, - radioButton: { - '& > label > span.MuiFormControlLabel-label': { - fontSize: '0.9rem', - }, - }, - legend: { - fontSize: '10px', - }, - advancedExpansionPanelSummary: { - 'justify-content': 'left', - '& div.MuiExpansionPanelSummary-content': { - 'flex-grow': 0, - }, - }, - }), - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/ConsoleBreadcrumbs/ConsoleBreadcrumbs.tsx b/ui/packages/platform/src/components/ConsoleBreadcrumbs/ConsoleBreadcrumbs.tsx deleted file mode 100644 index be8497c0..00000000 --- a/ui/packages/platform/src/components/ConsoleBreadcrumbs/ConsoleBreadcrumbs.tsx +++ /dev/null @@ -1,156 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component } from 'react' -import { NavLink } from 'react-router-dom' -import { Typography, Paper, Breadcrumbs } from '@material-ui/core' -import clsx from 'clsx' - -import { Head, createTitle as createTitleBase } from 'components/Head' -import { ConsoleBreadcrumbsProps } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { ClassesType, RefluxTypes } from '@postgres.ai/platform/src/components/types' - -import Store from '../../stores/store' -import Actions from '../../actions/actions' -import Urls from '../../utils/urls' - -interface ConsoleBreadcrumbsState { - data: { - userProfile: { - data: { - orgs: { - [org: string]: { - name: string - projects: { - [project: string]: { - name: string - } - } - } - } - } - } - } -} - -interface ConsoleBreadcrumbsWithStylesProps extends ConsoleBreadcrumbsProps { - classes: ClassesType -} - -const createTitle = (parts: string[]) => { - const filteredParts = parts.filter((part) => part !== 'Organizations') - return createTitleBase(filteredParts) -} - -class ConsoleBreadcrumbs extends Component< - ConsoleBreadcrumbsWithStylesProps, - ConsoleBreadcrumbsState -> { - unsubscribe: Function - componentDidMount() { - const that = this - - this.unsubscribe = (Store.listen as RefluxTypes["listen"]) (function () { - that.setState({ data: this.data }) - }) - Actions.refresh() - } - - componentWillUnmount() { - this.unsubscribe() - } - - render() { - const { classes, hasDivider = false, breadcrumbs } = this.props - const org = this.props.org ? this.props.org : null - const project = this.props.project ? this.props.project : null - const orgs = - this.state && - this.state.data && - this.state.data.userProfile.data && - this.state.data.userProfile.data.orgs - ? this.state.data.userProfile.data.orgs - : null - const paths = [] - let lastUrl = '' - - if (!breadcrumbs.length || Urls.isSharedUrl()) { - return null - } - - if (org && orgs && orgs[org]) { - if (orgs[org].name) { - paths.push({ name: 'Organizations', url: '/' }) - paths.push({ name: orgs[org].name, url: '/' + org }) - lastUrl = '/' + org - } - - if (project && orgs[org].projects && orgs[org].projects[project]) { - paths.push({ name: orgs[org].projects[project].name, url: null }) - lastUrl = '/' + org + '/' + project - } - } - - for (let i = 0; i < breadcrumbs.length; i++) { - if (breadcrumbs[i].url && breadcrumbs[i].url?.indexOf('/') === -1) { - breadcrumbs[i].url = lastUrl + '/' + breadcrumbs[i].url - lastUrl = breadcrumbs[i].url as string - } - breadcrumbs[i].isLast = i === breadcrumbs.length - 1 - paths.push(breadcrumbs[i]) - } - - return ( - <> - path.name))} /> - - - {paths.map( - ( - b: { name: string; url?: string | null; isLast?: boolean }, - index, - ) => { - return ( - - {b.url ? ( - - {b.name} - - ) : ( - - {b.name} - - )} - - ) - }, - )} - - - - ) - } -} - -export default ConsoleBreadcrumbs diff --git a/ui/packages/platform/src/components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper.tsx b/ui/packages/platform/src/components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper.tsx deleted file mode 100644 index e7414f06..00000000 --- a/ui/packages/platform/src/components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import { colors } from '@postgres.ai/shared/styles/colors' -import ConsoleBreadcrumbs from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbs' - -export interface ConsoleBreadcrumbsProps { - hasDivider?: boolean - org?: string | number - project?: string - breadcrumbs: { name: string; url?: string | null; isLast?: boolean }[] -} - -export const ConsoleBreadcrumbsWrapper = (props: ConsoleBreadcrumbsProps) => { - const useStyles = makeStyles( - { - pointerLink: { - cursor: 'pointer', - }, - breadcrumbsLink: { - maxWidth: 150, - textOverflow: 'ellipsis', - overflow: 'hidden', - display: 'block', - cursor: 'pointer', - whiteSpace: 'nowrap', - fontSize: '12px', - lineHeight: '14px', - textDecoration: 'none', - color: colors.consoleFadedFont, - }, - breadcrumbsItem: { - fontSize: '12px', - lineHeight: '14px', - color: colors.consoleFadedFont, - }, - breadcrumbsActiveItem: { - fontSize: '12px', - lineHeight: '14px', - color: '#000000', - }, - breadcrumbPaper: { - '& a, & a:visited': { - color: colors.consoleFadedFont, - }, - 'padding-bottom': '8px', - marginTop: '-10px', - 'font-size': '12px', - borderRadius: 0, - }, - breadcrumbPaperWithDivider: { - borderBottom: `1px solid ${colors.consoleStroke}`, - }, - }, - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/ConsoleButton/ConsoleButton.tsx b/ui/packages/platform/src/components/ConsoleButton/ConsoleButton.tsx deleted file mode 100644 index e1be66f7..00000000 --- a/ui/packages/platform/src/components/ConsoleButton/ConsoleButton.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component } from 'react' -import { Tooltip, Button } from '@material-ui/core' - -import { ClassesType } from '@postgres.ai/platform/src/components/types' -import { ConsoleButtonProps } from 'components/ConsoleButton/ConsoleButtonWrapper' - -interface ConsoleButtonWithStylesProps extends ConsoleButtonProps { - classes: ClassesType -} - -class ConsoleButton extends Component { - render() { - const { classes, title, children, ...other } = this.props - - // We have to use external tooltip component as disable button cannot show tooltip. - // Details: https://material-ui.com/components/tooltips/#disabled-elements. - return ( - - - - - - ) - } -} - -export default ConsoleButton diff --git a/ui/packages/platform/src/components/ConsoleButton/ConsoleButtonWrapper.tsx b/ui/packages/platform/src/components/ConsoleButton/ConsoleButtonWrapper.tsx deleted file mode 100644 index 23d4674b..00000000 --- a/ui/packages/platform/src/components/ConsoleButton/ConsoleButtonWrapper.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import ConsoleButton from 'components/ConsoleButton/ConsoleButton' - -export interface ConsoleButtonProps { - title: string - children: React.ReactNode - className?: string - disabled?: boolean - variant: 'text' | 'outlined' | 'contained' | undefined - color: 'primary' | 'secondary' | undefined - onClick: () => void - id?: string -} - -export const ConsoleButtonWrapper = (props: ConsoleButtonProps) => { - const useStyles = makeStyles( - { - tooltip: { - fontSize: '10px!important', - }, - }, - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/ConsolePageTitle.tsx b/ui/packages/platform/src/components/ConsolePageTitle.tsx deleted file mode 100644 index 4c170526..00000000 --- a/ui/packages/platform/src/components/ConsolePageTitle.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { - makeStyles, - Tooltip, - TextField, - InputAdornment, -} from '@material-ui/core' -import SearchIcon from '@material-ui/icons/Search' - -import { colors } from '@postgres.ai/shared/styles/colors' -import { icons } from '@postgres.ai/shared/styles/icons' - -interface ConsolePageTitleProps { - title: string - information?: string | JSX.Element - label?: string - actions?: JSX.Element[] | string[] - top?: boolean - filterProps?: { - filterValue: string - filterHandler: (event: React.ChangeEvent) => void - placeholder: string - } | null -} - -const useStyles = makeStyles( - { - pageTitle: { - flex: '0 0 auto', - '& > h1': { - display: 'inline-block', - fontSize: '16px', - lineHeight: '19px', - marginRight: '10px', - }, - 'border-top': '1px solid ' + colors.consoleStroke, - 'border-bottom': '1px solid ' + colors.consoleStroke, - 'padding-top': '8px', - 'padding-bottom': '8px', - display: 'block', - overflow: 'auto', - 'margin-bottom': '20px', - 'max-width': '100%', - }, - pageTitleTop: { - flex: '0 0 auto', - '& > h1': { - display: 'inline-block', - fontSize: '16px', - lineHeight: '19px', - marginRight: '10px', - }, - 'border-bottom': '1px solid ' + colors.consoleStroke, - 'padding-top': '0px', - 'margin-top': '-10px', - 'padding-bottom': '8px', - display: 'block', - overflow: 'auto', - 'margin-bottom': '20px', - }, - pageTitleActions: { - display: 'flex', - alignItems: 'center', - height: '100%', - float: 'right', - }, - pageTitleActionContainer: { - marginLeft: '10px', - display: 'inline-block', - height: "36px", - - "& > span, button": { - height: '100%', - }, - }, - tooltip: { - fontSize: '10px!important', - }, - label: { - backgroundColor: colors.primary.main, - color: colors.primary.contrastText, - display: 'inline-block', - borderRadius: 3, - fontSize: 10, - lineHeight: '12px', - padding: 2, - paddingLeft: 3, - paddingRight: 3, - verticalAlign: 'text-top', - }, - }, - { index: 1 }, -) - -const ConsolePageTitle = ({ - title, - information, - label, - actions, - top, - filterProps, -}: ConsolePageTitleProps) => { - const classes = useStyles() - - if (!title) { - return null - } - - return ( -
-

{title}

- {information ? ( - - {icons.infoIcon} - - ) : null} - {label ? {label} : null} - {(actions && actions?.length > 0) || filterProps ? ( - - {filterProps ? ( - - - - ), - }} - /> - ) : null} - {actions?.map((a, index) => { - return ( - - {a} - - ) - })} - - ) : null} -
- ) -} - -export default ConsolePageTitle diff --git a/ui/packages/platform/src/components/ContentLayout/DemoOrgNotice/index.tsx b/ui/packages/platform/src/components/ContentLayout/DemoOrgNotice/index.tsx deleted file mode 100644 index 2ada292b..00000000 --- a/ui/packages/platform/src/components/ContentLayout/DemoOrgNotice/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { makeStyles, Button } from '@material-ui/core' -import { useHistory } from 'react-router-dom' - -import { colors } from '@postgres.ai/shared/styles/colors' -import { icons } from '@postgres.ai/shared/styles/icons' - -import { ROUTES } from 'config/routes' - -const useStyles = makeStyles( - { - demoNoticeText: { - marginLeft: '0px', - display: 'inline-block', - position: 'relative', - backgroundColor: colors.blue, - color: colors.secondary2.darkDark, - width: '100%', - fontSize: '12px', - lineHeight: '24px', - fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', - paddingLeft: '10px', - paddingTop: '4px', - paddingBottom: '4px', - '& > svg': { - verticalAlign: 'baseline', - marginBottom: '-1px', - marginLeft: '0px', - marginRight: '4px', - }, - }, - demoOrgNoticeButton: { - padding: '2px', - paddingLeft: '6px', - paddingRight: '6px', - borderRadius: '3px', - marginLeft: '5px', - marginTop: '-2px', - height: '20px', - lineHeight: '20px', - fontSize: '12px', - fontWeight: 'bold', - }, - noWrap: { - whiteSpace: 'nowrap', - }, - }, - { index: 1 }, -) - -export const DemoOrgNotice = () => { - const classes = useStyles() - const history = useHistory() - - const goToOrgForm = () => history.push(ROUTES.CREATE_ORG.path) - - return ( -
- {icons.infoIconBlue} This is a demo organization. All the data here is public. - -
- ) -} diff --git a/ui/packages/platform/src/components/ContentLayout/DeprecatedApiBanner/index.tsx b/ui/packages/platform/src/components/ContentLayout/DeprecatedApiBanner/index.tsx deleted file mode 100644 index 47f00d86..00000000 --- a/ui/packages/platform/src/components/ContentLayout/DeprecatedApiBanner/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { makeStyles } from '@material-ui/core' - -import { Status } from '@postgres.ai/shared/components/Status' -import { GatewayLink } from '@postgres.ai/shared/components/GatewayLink' -import { colors } from '@postgres.ai/shared/styles/vars' - -const useStyles = makeStyles( - { - root: { - background: colors.status.warning, - color: colors.white, - fontSize: '12px', - padding: '4px 10px', - lineHeight: '1.5', - }, - status: { - color: 'inherit', - }, - link: { - color: 'inherit', - }, - }, - { index: 1 }, -) - -export const DeprecatedApiBanner = () => { - const classes = useStyles() - - return ( -
- - The version of your DBLab instance is deprecated. - {' '} - Some information about DBLab, disks, clones, and snapshots may be - unavailable. -
- Please upgrade your DBLab to  - - the latest available version - - . -
- ) -} diff --git a/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx b/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx deleted file mode 100644 index 8ef280ca..00000000 --- a/ui/packages/platform/src/components/ContentLayout/Footer/index.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { makeStyles, useMediaQuery } from '@material-ui/core' -import { GatewayLink } from '@postgres.ai/shared/components/GatewayLink' -import { useMemo } from 'react' -import { useLocation } from 'react-router-dom' - -import settings from 'utils/settings' -import cn from "classnames"; - -const useStyles = makeStyles( - (theme) => ({ - footer: { - flex: '0 0 auto', - backgroundColor: 'rgb(68, 79, 96)', - color: '#fff', - display: 'flex', - justifyContent: 'center', - padding: '16px 20px', - [theme.breakpoints.down('sm')]: { - padding: '12px 12px', - flexDirection: 'column', - }, - }, - hidden: { - display: 'none' - }, - footerCopyrightItem: { - marginRight: 50, - [theme.breakpoints.down('sm')]: { - marginBottom: 10, - }, - }, - footerLinks: { - display: 'flex', - [theme.breakpoints.down('sm')]: { - flexDirection: 'column', - flexWrap: 'wrap', - maxHeight: '80px', - }, - }, - footerItem: { - marginLeft: 10, - marginRight: 10, - color: '#fff', - '& a': { - color: '#fff', - textDecoration: 'none', - }, - '& a:hover': { - textDecoration: 'none', - }, - [theme.breakpoints.down('sm')]: { - marginLeft: 0, - marginBottom: 5, - }, - }, - footerItemSeparator: { - display: 'inline-block', - [theme.breakpoints.down('sm')]: { - display: 'none', - }, - }, - }), - { index: 1 }, -) - -export const Footer = () => { - const classes = useStyles() - const location = useLocation(); - const isMobile = useMediaQuery('(max-width:480px)'); - - const isAssistantPage = useMemo(() => { - return /^\/[^\/]+\/assistant(\/[^\/]+)?\/?$/.test(location.pathname); - }, [location.pathname]); - - return ( -
-
- {new Date().getFullYear()} © Postgres.AI -
-
-
- - Documentation - -
-
|
-
- - News - -
-
|
-
- - Terms of Service - -
-
|
-
- - Privacy Policy - -
-
|
-
- - Ask support - -
-
-
- ) -} diff --git a/ui/packages/platform/src/components/ContentLayout/index.tsx b/ui/packages/platform/src/components/ContentLayout/index.tsx deleted file mode 100644 index 6b4e2eca..00000000 --- a/ui/packages/platform/src/components/ContentLayout/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import React from 'react' -import { useLocation, useRouteMatch } from 'react-router-dom' -import clsx from 'clsx' -import { observer } from 'mobx-react-lite' - -import { ROUTES } from 'config/routes' -import settings from 'utils/settings' -import { bannersStore } from 'stores/banners' - -import { DemoOrgNotice } from './DemoOrgNotice' -import { DeprecatedApiBanner } from './DeprecatedApiBanner' -import { Footer } from './Footer' - -import styles from './styles.module.scss' -import cn from "classnames"; - -type Props = { - children: React.ReactNode -} - -export const ContentLayout = React.memo(observer((props: Props) => { - const { children } = props - - const location = useLocation(); - - const isOrgJoeInstance = Boolean( - useRouteMatch(ROUTES.ORG.JOE_INSTANCES.JOE_INSTANCE.createPath()), - ) - - const isProjectJoeInstance = Boolean( - useRouteMatch(ROUTES.ORG.PROJECT.JOE_INSTANCES.JOE_INSTANCE.createPath()), - ) - - const isAssistantPage = Boolean( - useRouteMatch(ROUTES.ORG.PROJECT.ASSISTANT.createPath()) - ) - - const isDemoOrg = Boolean(useRouteMatch(`/${settings.demoOrgAlias}`)) - - const isHiddenFooter = isOrgJoeInstance || isProjectJoeInstance - - return ( -
- {isDemoOrg && } - { bannersStore.isOpenDeprecatedApi && } - -
-
- {children} -
-
-
-
- ) -})) diff --git a/ui/packages/platform/src/components/ContentLayout/styles.module.scss b/ui/packages/platform/src/components/ContentLayout/styles.module.scss deleted file mode 100644 index 5a40bfc2..00000000 --- a/ui/packages/platform/src/components/ContentLayout/styles.module.scss +++ /dev/null @@ -1,46 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -@import '@postgres.ai/shared/styles/mixins'; - -.root { - display: flex; - flex-direction: column; - flex: 1 1 100%; - padding-top: 40px; - width: 100%; - // Flexbox bug fix - https://bugzilla.mozilla.org/show_bug.cgi?id=1086218#c4. - min-width: 0; -} - -.rootAssistant { - @media (max-width: 480px) { - height: 100dvh; - } -} - -.wrapper { - flex: 1 1 100%; - overflow: auto; - display: flex; - flex-direction: column; -} - -.content { - flex: 1 1 100%; - display: flex; - flex-direction: column; - padding: 20px; - - &.fullScreen { - flex-shrink: 0; - } - - @include sm { - padding: 20px 12px; - } -} diff --git a/ui/packages/platform/src/components/CreateClusterCards/CreateClusterCards.tsx b/ui/packages/platform/src/components/CreateClusterCards/CreateClusterCards.tsx deleted file mode 100644 index 91fcabed..00000000 --- a/ui/packages/platform/src/components/CreateClusterCards/CreateClusterCards.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import classNames from 'classnames' -import { StubContainer } from '@postgres.ai/shared/components/StubContainer' -import { icons } from '@postgres.ai/shared/styles/icons' -import { ConsoleButtonWrapper } from 'components/ConsoleButton/ConsoleButtonWrapper' -import { ProductCardWrapper } from 'components/ProductCard/ProductCardWrapper' -import { DashboardProps } from 'components/Dashboard/DashboardWrapper' - -import Urls from '../../utils/urls' -import { messages } from '../../assets/messages' -import { useStyles } from 'components/CreateDbLabCards/CreateDbLabCards' - -export const CreateClusterCards = ({ - isModal, - props, - dblabPermitted, -}: { - isModal?: boolean - props: DashboardProps - dblabPermitted: boolean | undefined -}) => { - const classes = useStyles() - - const createClusterInstanceButton = (provider: string) => { - props.history.push(Urls.linkClusterInstanceAdd(props, provider)) - } - - const CreateButton = ({ type, title }: { type: string; title: string }) => ( - createClusterInstanceButton(type)} - title={dblabPermitted ? title : messages.noPermission} - > - {type === 'create' ? 'Create' : 'Install'} - - ) - - const productData = [ - { - title: 'Create Postgres Cluster in your cloud', - renderDescription: () => ( - <> -

- Supported cloud platforms include AWS, GCP, Digital Ocean, and - Hetzner Cloud. -

-

All components are installed within your cloud account.

-

Your data remains secure and never leaves your infrastructure.

- - ), - icon: icons.createDLEIcon, - actions: [ - { - id: 'createDblabInstanceButton', - content: ( - - ), - }, - ], - }, - { - title: 'BYOM (Bring Your Own Machines)', - renderDescription: () => ( - <> -

- Install on your existing resources, regardless of the machine or - location. Compatible with both cloud and bare metal infrastructures. - Your data remains secure and never leaves your infrastructure. -

-

Requirements:

-
    -
  • - Three or more servers running a supported Linux distro: Ubuntu - (20.04/22.04), Debian (11/12), CentOS Stream (8/9), Rocky Linux - (8/9), AlmaLinux (8/9), or Red Hat Enterprise Linux (8/9). -
  • -
  • Internet connectivity
  • -
- - ), - icon: icons.installDLEIcon, - actions: [ - { - id: 'createDblabInstanceButton', - content: ( - - ), - }, - ], - }, - ] - - return ( - - {productData.map((product) => ( - -
{product.renderDescription()}
-
- ))} -
- ) -} diff --git a/ui/packages/platform/src/components/CreateDbLabCards/CreateDbLabCards.tsx b/ui/packages/platform/src/components/CreateDbLabCards/CreateDbLabCards.tsx deleted file mode 100644 index 8529fd3a..00000000 --- a/ui/packages/platform/src/components/CreateDbLabCards/CreateDbLabCards.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import classNames from 'classnames' -import { makeStyles } from '@material-ui/core' -import { StubContainer } from '@postgres.ai/shared/components/StubContainer' -import { icons } from '@postgres.ai/shared/styles/icons' -import { ConsoleButtonWrapper } from 'components/ConsoleButton/ConsoleButtonWrapper' -import { ProductCardWrapper } from 'components/ProductCard/ProductCardWrapper' -import { DashboardProps } from 'components/Dashboard/DashboardWrapper' - -import Urls from '../../utils/urls' -import { messages } from '../../assets/messages' - -export const useStyles = makeStyles((theme) => ({ - zeroMaxHeight: { - maxHeight: 0, - }, - stubContainerProjects: { - marginRight: '-20px', - padding: '0 40px', - alignItems: 'initial !important', - - '& > div:nth-child(1), & > div:nth-child(2)': { - minHeight: '350px', - }, - - [theme.breakpoints.down('sm')]: { - flexDirection: 'column', - }, - }, - productCardProjects: { - flex: '1 1 0', - marginRight: '20px', - height: 'maxContent', - gap: 20, - maxHeight: '100%', - '& ul': { - marginBlockStart: '-10px', - paddingInlineStart: '30px', - }, - - '& svg': { - width: '206px', - height: '130px', - }, - - '&:nth-child(1) svg': { - marginBottom: '-5px', - }, - - '&:nth-child(2) svg': { - marginBottom: '-20px', - }, - - '& li': { - listStyleType: 'none', - position: 'relative', - - '&::before': { - content: '"-"', - position: 'absolute', - left: '-10px', - top: '0', - }, - }, - - [theme.breakpoints.down('sm')]: { - flex: '100%', - marginTop: '20px', - minHeight: 'auto !important', - - '&:nth-child(1) svg': { - marginBottom: 0, - }, - - '&:nth-child(2) svg': { - marginBottom: 0, - }, - }, - }, -})) - -export const CreatedDbLabCards = ({ - isModal, - props, - dblabPermitted, -}: { - isModal?: boolean - props: DashboardProps - dblabPermitted: boolean | undefined -}) => { - const classes = useStyles() - - const createDblabInstanceButtonHandler = (provider: string) => { - props.history.push(Urls.linkDbLabInstanceAdd(props, provider)) - } - - const CreateButton = ({ type, title }: { type: string; title: string }) => ( - createDblabInstanceButtonHandler(type)} - title={dblabPermitted ? title : messages.noPermission} - > - {type === 'create' ? 'Create' : 'Install'} - - ) - - const productData = [ - { - title: 'Create DBLab in your cloud', - renderDescription: () => ( - <> -

- Supported cloud platforms include AWS, GCP, Digital Ocean, and - Hetzner Cloud. -

-

All components are installed within your cloud account.

-

Your data remains secure and never leaves your infrastructure.

- - ), - icon: icons.createDLEIcon, - actions: [ - { - id: 'createDblabInstanceButton', - content: ( - - ), - }, - ], - }, - { - title: 'BYOM (Bring Your Own Machine)', - renderDescription: () => ( - <> -

- Install on your existing resources, regardless of the machine or - location. Compatible with both cloud and bare metal infrastructures. - Your data remains secure and never leaves your infrastructure. -

-

Requirements:

-
    -
  • Ubuntu 20.04 or newer
  • -
  • Internet connectivity
  • -
- - ), - icon: icons.installDLEIcon, - actions: [ - { - id: 'createDblabInstanceButton', - content: ( - - ), - }, - ], - }, - ] - - return ( - - {productData.map((product) => ( - -
{product.renderDescription()}
-
- ))} -
- ) -} diff --git a/ui/packages/platform/src/components/DBLabSettingsForm/DBLabSettingsForm.tsx b/ui/packages/platform/src/components/DBLabSettingsForm/DBLabSettingsForm.tsx deleted file mode 100644 index 9224cad0..00000000 --- a/ui/packages/platform/src/components/DBLabSettingsForm/DBLabSettingsForm.tsx +++ /dev/null @@ -1,413 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react' -import { Link } from '@postgres.ai/shared/components/Link2' -import { - Grid, - Button, - FormControl, - FormControlLabel, - makeStyles, - Typography, TextField -} from '@material-ui/core' -import { useFormik } from "formik"; -import * as Yup from 'yup'; -import Store from '../../stores/store' -import Actions from '../../actions/actions' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import ConsolePageTitle from '../ConsolePageTitle' -import { DBLabSettingsFormProps } from './DBLabSettingsFormWrapper' -import { styles } from "@postgres.ai/shared/styles/styles"; -import { PageSpinner } from "@postgres.ai/shared/components/PageSpinner"; -import { WarningWrapper } from "../Warning/WarningWrapper"; -import { messages } from "../../assets/messages"; -import { ExternalIcon } from "@postgres.ai/shared/icons/External"; -import Checkbox from '@mui/material/Checkbox/Checkbox' -import { hoursToPgInterval, pgIntervalToHours } from 'utils/utils'; - -type DBLabSettingsState = { - data: { - auth: { - token: string | null - } | null - orgProfile: { - isUpdating: boolean - error: boolean - updateError: boolean - errorMessage: string | undefined - errorCode: number | undefined - updateErrorMessage: string | null - isProcessing: boolean - orgId: number | null - updateErrorFields: string[] - data: { - dblab_low_disk_space_notifications_threshold_percent: number | null - dblab_old_clones_notifications_threshold_hours: string | null - } - } | null - } | null -} - -interface NotificationsSettings { - isLowDiskSpaceCheckboxActive: boolean; - isOldClonesCheckboxActive: boolean; - lowDiskSpaceThreshold: number | null | undefined; - oldClonesThreshold: number | null | undefined; -} - -export interface FormValues { - notifications: NotificationsSettings; -} - -const useStyles = makeStyles( - { - container: { - ...(styles.root as Object), - display: 'flex', - 'flex-wrap': 'wrap', - 'min-height': 0, - '&:not(:first-child)': { - 'margin-top': '20px', - }, - }, - formContainer: { - flexWrap: 'nowrap' - }, - textField: { - ...styles.inputField, - marginBottom: 16, - marginTop: 8 - }, - instructionsField: { - ...styles.inputField, - }, - selectField: { - marginTop: 4, - - }, - label: { - color: '#000!important', - fontWeight: 'bold', - }, - updateButtonContainer: { - marginTop: 20, - textAlign: 'left', - }, - unlockNote: { - marginTop: 8, - '& ol': { - paddingLeft: 24, - marginTop: 6, - marginBottom: 0 - } - }, - externalIcon: { - width: 14, - height: 14, - marginLeft: 4, - transform: 'translateY(2px)', - }, - testConnectionButton: { - marginRight: 16 - }, - eventRow: { - display: 'flex', - alignItems: 'center', - marginBottom: '10px', - } - }, - { index: 1 }, -) - -const validationSchema = Yup.object({ - notifications: Yup.object({ - isLowDiskSpaceCheckboxActive: Yup.boolean().optional(), - isOldClonesCheckboxActive: Yup.boolean().optional(), - lowDiskSpaceThreshold: Yup.number() - .nullable() - .when('isLowDiskSpaceCheckboxActive', { - is: true, - then: (schema) => schema.required('Please enter a threshold value.').min(1, 'Must be at least 1'), - otherwise: (schema) => schema.nullable(), - }), - oldClonesThreshold: Yup.number() - .nullable() - .when('isOldClonesCheckboxActive', { - is: true, - then: (schema) => schema.required('Please enter a threshold value.').min(1, 'Must be at least 1'), - otherwise: (schema) => schema.nullable(), - }), - }), -}); - -const LOW_DISK_SPACE_THRESHOLD_DEFAULT = 20; -const OLD_CLONES_THRESHOLD_DEFAULT = 24; - -const DBLabSettingsForm: React.FC = (props) => { - const { orgPermissions, orgData, orgId, org, project } = props; - const classes = useStyles(); - const [data, setData] = useState(null); - - useEffect(() => { - const unsubscribe = Store.listen(function () { - const newStoreData = this.data; - - if (JSON.stringify(newStoreData) !== JSON.stringify(data)) { - const auth = newStoreData?.auth || null; - const orgProfile = newStoreData?.orgProfile || null; - - if ( - auth?.token && - orgProfile && - orgProfile.orgId !== orgId && - !orgProfile.isProcessing - ) { - Actions.getOrgs(auth.token, orgId); - } - - - setData(newStoreData); - } - }); - - Actions.refresh(); - - return () => { - unsubscribe(); - }; - }, [orgId, data, props.match.params.projectId]); - - const isDBLabSettingsAvailable = useMemo(() => { - const privileged_until = orgData?.priveleged_until; - return !!(orgData && privileged_until && new Date(privileged_until) > new Date() && orgData?.consulting_type === 'enterprise'); - - }, [orgData]) - - - - const formik = useFormik({ - enableReinitialize: true, - initialValues: { - notifications: { - isLowDiskSpaceCheckboxActive: Boolean(data?.orgProfile?.data?.dblab_low_disk_space_notifications_threshold_percent), - isOldClonesCheckboxActive: Boolean(data?.orgProfile?.data?.dblab_old_clones_notifications_threshold_hours), - lowDiskSpaceThreshold: data?.orgProfile?.data?.dblab_low_disk_space_notifications_threshold_percent || LOW_DISK_SPACE_THRESHOLD_DEFAULT, - oldClonesThreshold: pgIntervalToHours(data?.orgProfile?.data?.dblab_old_clones_notifications_threshold_hours) || OLD_CLONES_THRESHOLD_DEFAULT, - }, - }, - validationSchema, - onSubmit: async (values, { setSubmitting }) => { - const errors = await formik.validateForm(); - - if (Object.keys(errors).length > 0) { - console.error('Validation errors:', errors); - setSubmitting(false); - return; // Stop submission if there are errors - } - - const currentOrgId = orgId || null; - const auth = data?.auth || null; - - let params: { dblab_low_disk_space_notifications_threshold_percent: number | null, dblab_old_clones_notifications_threshold_hours: string | null } = { - dblab_low_disk_space_notifications_threshold_percent: null, - dblab_old_clones_notifications_threshold_hours: null - } - - if (values.notifications.isLowDiskSpaceCheckboxActive) { - params.dblab_low_disk_space_notifications_threshold_percent = values.notifications.lowDiskSpaceThreshold as number; - } - - if (values.notifications.isOldClonesCheckboxActive) { - params.dblab_old_clones_notifications_threshold_hours = hoursToPgInterval(values.notifications.oldClonesThreshold as number); - } - - if (auth) { - try { - await Actions.updateDBLabSettings(auth.token, currentOrgId, params); - } catch (error) { - const errorMessage = `Error updating DBLab settings: ${error}`; - Actions.showNotification(errorMessage, 'error'); - console.error('Error updating DBLab settings:', error); - } finally { - setSubmitting(false); - } - } - } - }); - - - const breadcrumbs = ( - - ); - - const pageTitle = ; - - if (orgPermissions && !orgPermissions.settingsOrganizationUpdate) { - return ( - <> - {breadcrumbs} - {pageTitle} - {messages.noPermissionPage} - - ); - } - - if (!data || (data && data.orgProfile && data.orgProfile.isProcessing)) { - return ( -
- {breadcrumbs} - {pageTitle} - -
- ); - } - - return ( - <> - {breadcrumbs} - {pageTitle} -
- - - - {!isDBLabSettingsAvailable && - - Become an Enterprise customer - - -  to unlock DBLab settings - } - - -

E-mail notifications

- - formik.setFieldValue( - 'notifications.isLowDiskSpaceCheckboxActive', - e.target.checked - ) - } - /> - } - label="Notify organization administrators about low disk space" // TODO: @Nik, change text - disabled={!isDBLabSettingsAvailable} - /> - formik.setFieldValue('notifications.lowDiskSpaceThreshold', e.target.value)} - value={formik.values.notifications.lowDiskSpaceThreshold} - margin="normal" - fullWidth - inputProps={{ - name: 'lowDiskSpaceThreshold', - id: 'lowDiskSpaceThresholdTextField', - min: 1, - max: 99, - inputMode: 'numeric', - pattern: '[0-9]*' - }} - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - error={ - formik.touched.notifications?.lowDiskSpaceThreshold && - Boolean(formik.errors.notifications?.lowDiskSpaceThreshold) - } - helperText={ - formik.touched.notifications?.lowDiskSpaceThreshold && - formik.errors.notifications?.lowDiskSpaceThreshold - } - /> - - formik.setFieldValue( - 'notifications.isOldClonesCheckboxActive', - e.target.checked - ) - } - /> - } - label="Notify organization members about old clones" // TODO: @Nik, change text - disabled={!isDBLabSettingsAvailable} - /> - formik.setFieldValue('notifications.oldClonesThreshold', e.target.value)} - value={formik.values.notifications.oldClonesThreshold} - margin="normal" - fullWidth - inputProps={{ - name: 'oldClonesThreshold', - id: 'oldClonesThresholdTextField', - min: 1, - inputMode: 'numeric', - pattern: '[0-9]*' - }} - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - error={ - formik.touched.notifications?.oldClonesThreshold && - Boolean(formik.errors.notifications?.oldClonesThreshold) - } - helperText={ - formik.touched.notifications?.oldClonesThreshold && - formik.errors.notifications?.oldClonesThreshold - } - /> -
-
-
- - - -
-
-
- - ); -}; - -export default DBLabSettingsForm diff --git a/ui/packages/platform/src/components/DBLabSettingsForm/DBLabSettingsFormWrapper.tsx b/ui/packages/platform/src/components/DBLabSettingsForm/DBLabSettingsFormWrapper.tsx deleted file mode 100644 index e9fd4075..00000000 --- a/ui/packages/platform/src/components/DBLabSettingsForm/DBLabSettingsFormWrapper.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import DBLabSettingsForm from "./DBLabSettingsForm"; - -export interface DBLabSettingsFormProps { - mode?: string | undefined - project?: string | undefined - org?: string | number - orgId?: number - orgPermissions?: { - settingsOrganizationUpdate?: boolean - } - orgData?: { - priveleged_until: Date - chats_private_allowed: boolean - consulting_type: string | null - } - match: { - params: { - project?: string - projectId?: string | number | undefined - org?: string - } - } -} - - - -export const DBLabSettingsFormWrapper = (props: DBLabSettingsFormProps) => { - return -} diff --git a/ui/packages/platform/src/components/Dashboard/Dashboard.tsx b/ui/packages/platform/src/components/Dashboard/Dashboard.tsx deleted file mode 100644 index c587878f..00000000 --- a/ui/packages/platform/src/components/Dashboard/Dashboard.tsx +++ /dev/null @@ -1,618 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component, MouseEvent } from 'react' -import { NavLink } from 'react-router-dom' -import Brightness1Icon from '@material-ui/icons/Brightness1' -import { - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Button, - Grid, -} from '@material-ui/core' -import ReactMarkdown from 'react-markdown' -import rehypeRaw from 'rehype-raw' -import remarkGfm from 'remark-gfm' - -import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer' -import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner' -import { StubContainer } from '@postgres.ai/shared/components/StubContainer' -import { - ClassesType, - RefluxTypes, -} from '@postgres.ai/platform/src/components/types' - -import { ROUTES } from 'config/routes' - -import Actions from '../../actions/actions' -import ConsolePageTitle from '../ConsolePageTitle' -import { ErrorWrapper } from 'components/Error/ErrorWrapper' -import { GatewayLink } from '@postgres.ai/shared/components/GatewayLink' -import Store from '../../stores/store' -import Urls from '../../utils/urls' -import settings from '../../utils/settings' -import format from '../../utils/format' - -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { ConsoleButtonWrapper } from 'components/ConsoleButton/ConsoleButtonWrapper' -import { ProductCardWrapper } from 'components/ProductCard/ProductCardWrapper' -import { DashboardProps } from 'components/Dashboard/DashboardWrapper' -import { FilteredTableMessage } from 'components/AccessTokens/FilteredTableMessage/FilteredTableMessage' -import { CreatedDbLabCards } from 'components/CreateDbLabCards/CreateDbLabCards' -import { convertThread } from "../../api/bot/convertThread"; - -interface DashboardWithStylesProps extends DashboardProps { - classes: ClassesType -} - -interface DashboardState { - filterValue: string - data: { - auth: { - token: string - } | null - projects: { - error: boolean - isProcessing: boolean - orgId: number - data: { - label: string - project_label_or_name: string - id: number - name: string - alias: string - }[] - } | null - orgId: number - userProfile: { - data: { - platform_onboarding_text: string - orgs: { - [org: string]: { - is_blocked: boolean - created_at: string - id: number - alias: string - name: string - onboarding_text: string - projects: Object - } - } - } - isProcessing: boolean - isProcessed: boolean - error: boolean - } | null - useDemoData: { - isProcessing: boolean - isProcessed: boolean - } | null - dashboard: { - profileUpdateInitAfterDemo: boolean - } | null - } -} - -class Dashboard extends Component { - isThreadConverted = false; - unsubscribe: Function - componentDidMount() { - const that = this - const orgId = this.props.orgId - const onlyProjects = this.props.onlyProjects - - this.unsubscribe = (Store.listen as RefluxTypes['listen'])(function () { - that.setState({ data: this.data }) - - const auth: DashboardState['data']['auth'] = - this.data && this.data.auth ? this.data.auth : null - const userProfile: DashboardState['data']['userProfile'] = - this.data && this.data.userProfile ? this.data.userProfile : null - - const cookieName = "pgai_tmp_thread_id="; - const cookies = document.cookie.split(';').map(cookie => cookie.trim()); - const pgaiTmpThreadId = cookies.find(cookie => cookie.startsWith(cookieName))?.substring(cookieName.length) || null; - if (pgaiTmpThreadId && !that.isThreadConverted) { - that.isThreadConverted = true; - try { - convertThread(pgaiTmpThreadId) - .then(({response, error}) => { - if (response?.final_thread_id) { - document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname.split('.').slice(-2).join('.')}`; - if (userProfile && userProfile.data && userProfile.data.orgs) { - if (userProfile.data.orgs.hasOwnProperty('demo')) { - that.props.history.push(`demo/assistant/${response.final_thread_id}`); - } - } - } - }) - } catch (error) { - console.error('Error converting thread:', error); - } - } else { - document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname.split('.').slice(-2).join('.')}`; - } - - if (onlyProjects) { - const projects: DashboardState['data']['projects'] = - this.data && this.data.projects ? this.data.projects : null - - if ( - auth && - auth.token && - !projects?.isProcessing && - !projects?.error && - !that.state - ) { - Actions.getProjects(auth.token, orgId) - } - - if ( - auth && - !that.state && - !userProfile?.isProcessing && - !userProfile?.error - ) { - Actions.getUserProfile(auth.token) - } - } else { - const useDemoData = - this.data && this.data.useDemoData ? this.data.useDemoData : null - const profileUpdateInitAfterDemo = - this.data && this.data.dashboard - ? this.data.dashboard.profileUpdateInitAfterDemo - : null - - if ( - auth && - auth.token && - ((!userProfile?.isProcessed && - !userProfile?.isProcessing && - !userProfile?.error) || - (!profileUpdateInitAfterDemo && - useDemoData.isProcessed && - !useDemoData.error)) - ) { - if (useDemoData.isProcessed) { - this.data.dashboard.profileUpdateInitAfterDemo = true - } - - Actions.getUserProfile(auth.token) - } - } - }) - - Actions.refresh() - } - - componentWillUnmount() { - this.unsubscribe() - } - - handleClick = ( - _: MouseEvent, - alias?: string, - ) => { - this.props.history.push('/' + alias) - } - - useDemoDataButtonHandler = () => { - const auth = - this.state.data && this.state.data.auth ? this.state.data.auth : null - Actions.useDemoData(auth?.token) - } - - addOrgButtonHandler = () => { - this.props.history.push(ROUTES.CREATE_ORG.path) - } - - addCheckupAgentButtonHandler = () => { - this.props.history.push(Urls.linkCheckupAgentAdd(this.props)) - } - - dblabInstancesButtonHandler = (org: string | number, project: string) => { - return () => { - this.props.history.push(Urls.linkDbLabInstances({ org, project })) - } - } - - joeInstancesButtonHandler = (org: string | number, project: string) => { - return () => { - this.props.history.push(Urls.linkJoeInstances({ org, project })) - } - } - - checkupReportsButtonHandler = (org: string | number, project: string) => { - return () => { - this.props.history.push(Urls.linkReports({ org, project })) - } - } - - filterOrgsInputHandler = (event: React.ChangeEvent) => { - this.setState({ filterValue: event.target.value }) - } - - render() { - const renderProjects = this.props.onlyProjects - - if (renderProjects) { - return this.renderProjects() - } - - // TODO(anatoly): Move organization to a separate page component. - return this.renderOrgs() - } - - renderProjects() { - const { classes } = this.props - const org = this.props.org as string | number - const orgId = this.props.orgId - const projectsData = - this.state && this.state.data && this.state.data.projects - ? this.state.data.projects - : null - - const breadcrumbs = ( - - ) - - const pageTitle = ( - - ) - - if (projectsData && projectsData.error) { - return ( - <> - {breadcrumbs} - - - ) - } - - if (!projectsData || !projectsData.data || projectsData.orgId !== orgId) { - return ( - <> - {breadcrumbs} - - - ) - } - - const projects = projectsData.data - - const dblabPermitted = this.props.orgPermissions?.dblabInstanceCreate - - let table = ( - - ) - - if (projects.length > 0) { - table = ( - - - - - Project - Activity - - - - {projects.map((p) => { - return ( - - - {p.project_label_or_name || p.label || p.name} - - - - - - - - ) - })} - -
-
- ) - } - - let onboarding = null - if ( - this.state.data && - this.state.data.userProfile && - this.state.data.userProfile.data && - this.state.data.userProfile.data.orgs && - this.state.data.userProfile.data.orgs[org] && - this.state.data.userProfile.data.orgs[org].projects && - this.state.data.userProfile.data.orgs[org].onboarding_text - ) { - onboarding = ( -
- - -
-

Getting started

- { - const { href, target, children } = props - return ( - - {String(children)} - - ) - }, - }} - /> -
-
-
-
- ) - } - - return ( -
- {breadcrumbs} - - {pageTitle} - - {onboarding} - - {table} -
- ) - } - - renderOrgs() { - const { classes } = this.props - const profile = - this.state && this.state.data ? this.state.data.userProfile : null - const useDemoData = - this.state && this.state.data ? this.state.data.useDemoData : null - const profileUpdateInitAfterDemo = - this.state && this.state.data && this.state.data.dashboard - ? this.state.data.dashboard.profileUpdateInitAfterDemo - : null - - const filteredItems = - profile?.data?.orgs && - Object.keys(profile?.data?.orgs)?.filter( - (org) => - org - ?.toLowerCase() - .indexOf((this.state.filterValue || '')?.toLowerCase()) !== -1, - ) - - // Show organizations. - if (this.state && this.state.data.projects?.error) { - return ( -
- -
- ) - } - - if ( - !profile || - profile.isProcessing || - (profile && !profile.data) || - !useDemoData || - useDemoData.isProcessing || - (useDemoData.isProcessed && !profileUpdateInitAfterDemo) - ) { - return ( - <> - - - ) - } - - const useDemoDataButton = ( - - Join demo organization - - ) - - const createOrgButton = ( - - Create new organization - - ) - - const orgsPlaceholder = ( - - -

- An organization represents a workspace for you and your colleagues. - Organizations allow you to manage users and collaborate across - multiple projects. -

-

- You can create a new organization, join the demo organization or ask - existing members of your organization to invite you. -

-
-
- ) - - const pageActions = [] - if ( - Object.keys(profile?.data?.orgs).length > 0 && - (!profile.data?.orgs || !profile.data?.orgs[settings.demoOrgAlias]) - ) { - pageActions.push(useDemoDataButton) - } - - if (Object.keys(profile?.data?.orgs).length > 0) { - pageActions.push(createOrgButton) - } - - return ( -
- 0 - ? { - filterValue: this.state.filterValue, - filterHandler: this.filterOrgsInputHandler, - placeholder: 'Search organizations by name', - } - : null - } - /> - {profile.data?.orgs && filteredItems && filteredItems.length > 0 ? ( - - - - - - Organization - - Projects count - Status - Created at - - - - {filteredItems.map((index) => { - return ( - - this.handleClick(event, profile.data?.orgs[index].alias) - } - style={{ cursor: 'pointer' }} - data-org-id={profile.data?.orgs[index].id} - data-org-alias={profile.data?.orgs[index].alias} - > - - - {profile.data?.orgs[index].name} - - - - - {profile.data?.orgs[index].projects - ? Object.keys(profile.data?.orgs[index]?.projects) - .length - : '0'} - - - - - - - {format.formatDate( - profile.data?.orgs[index].created_at, - ) || '-'} - - - ) - })} - -
-
- ) : ( - - this.setState({ - filterValue: '', - }) - } - /> - )} -
- ) - } -} - -export default Dashboard diff --git a/ui/packages/platform/src/components/Dashboard/DashboardWrapper.tsx b/ui/packages/platform/src/components/Dashboard/DashboardWrapper.tsx deleted file mode 100644 index b279a66d..00000000 --- a/ui/packages/platform/src/components/Dashboard/DashboardWrapper.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import { colors } from '@postgres.ai/shared/styles/colors' -import { RouteComponentProps } from 'react-router' -import Dashboard from 'components/Dashboard/Dashboard' - -export interface DashboardProps { - org?: string | number - orgId?: number - onlyProjects?: boolean - history: RouteComponentProps['history'] - project?: string | undefined - orgPermissions?: { - dblabInstanceCreate?: boolean - checkupReportConfigure?: boolean - } -} - -export const DashboardWrapper = (props: DashboardProps) => { - const useStyles = makeStyles( - (theme) => ({ - orgsHeader: { - position: 'relative', - }, - newOrgBtn: { - position: 'absolute', - top: 0, - right: 10, - }, - nameColumn: { - 'word-wrap': 'break-word', - [theme.breakpoints.down('sm')]: { - maxWidth: 'calc(100vw - 150px)', - }, - [theme.breakpoints.up('md')]: { - maxWidth: 'calc(100vw - 350px)', - }, - [theme.breakpoints.up('lg')]: { - maxWidth: 'calc(100vw - 350px)', - }, - '& > a': { - color: 'black', - textDecoration: 'none', - }, - '& > a:hover': { - color: 'black', - textDecoration: 'none', - }, - }, - cell: { - '& > a': { - color: 'black', - textDecoration: 'none', - }, - '& > a:hover': { - color: 'black', - textDecoration: 'none', - }, - }, - activityButton: { - '&:not(:first-child)': { - marginLeft: '15px', - }, - }, - onboardingCard: { - border: '1px solid ' + colors.consoleStroke, - borderRadius: 3, - padding: 15, - '& h1': { - fontSize: '16px', - margin: '0', - }, - }, - onboarding: { - '& ul': { - paddingInlineStart: '20px', - }, - }, - filterOrgsInput: { - width: '100%', - - '& .MuiOutlinedInput-input': { - width: '200px', - }, - }, - blockedStatus: { - color: colors.state.error, - fontSize: '1.1em', - verticalAlign: 'middle', - '& svg': { - marginTop: '-3px', - }, - }, - validStatus: { - color: colors.state.ok, - fontSize: '1.1em', - verticalAlign: 'middle', - '& svg': { - marginTop: '-3px', - }, - }, - flexContainer: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - height: '100%', - gap: 40, - marginTop: '20px', - - '& > div': { - maxWidth: '300px', - width: '100%', - height: '100%', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - border: '1px solid #e0e0e0', - padding: '20px', - borderRadius: '4px', - cursor: 'pointer', - fontSize: '15px', - transition: 'border 0.3s ease-in-out', - - '&:hover': { - border: '1px solid #FF6212', - }, - }, - }, - }), - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx deleted file mode 100644 index 4fbc442b..00000000 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import { Button, makeStyles } from '@material-ui/core' -import { Box } from '@mui/material' -import { useEffect, useState } from 'react' - -import { ErrorStub } from '@postgres.ai/shared/components/ErrorStub' -import { Spinner } from '@postgres.ai/shared/components/Spinner' -import { SyntaxHighlight } from '@postgres.ai/shared/components/SyntaxHighlight' - -import { getCloudImages } from 'api/cloud/getCloudImages' -import { getOrgKeys } from 'api/cloud/getOrgKeys' - -import { InstanceFormCreation } from 'components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation' -import { - getGcpAccountContents, - getNetworkSubnet, - getPlaybookCommand, -} from 'components/DbLabInstanceForm/utils' -import { - cloneRepositoryCommand, - getAnsibleInstallationCommand, -} from 'components/DbLabInstanceInstallForm/utils' - -import { - cloneClusterRepositoryCommand, - getClusterPlaybookCommand, -} from 'components/PostgresClusterForm/utils' -import { useCloudProviderProps } from 'hooks/useCloudProvider' - -export const formStyles = makeStyles({ - marginTop: { - marginTop: '20px !important', - }, - marginBottom: { - marginBottom: '20px', - display: 'block', - }, - maxContentWidth: { - maxWidth: '800px', - }, - spinner: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - }, - buttonSpinner: { - marginRight: '8px', - color: '#fff', - }, - title: { - fontWeight: 600, - fontSize: '15px', - margin: '10px 0', - }, - mainTitle: { - fontWeight: 600, - fontSize: '20px', - borderBottom: '1px solid #eee', - margin: '0 0 10px 0', - paddingBottom: '10px', - }, - note: { - fontSize: '12px', - margin: '0 0 10px 0', - color: '#777', - }, - code: { - backgroundColor: '#eee', - borderRadius: '3px', - padding: '0 3px', - marginLeft: '0.25em', - }, - ul: { - paddingInlineStart: '30px', - - '& li': { - marginBottom: '5px', - }, - }, - important: { - fontWeight: 600, - margin: 0, - }, - containerMargin: { - margin: '20px 0', - }, - smallMarginTop: { - marginBottom: '10px', - }, -}) - -export const InstanceDocumentation = ({ - firstStep, - firsStepDescription, - documentation, - secondStep, - snippetContent, - classes, -}: { - firstStep: string - firsStepDescription?: React.ReactNode - documentation: string - secondStep: React.ReactNode - snippetContent: string - classes: ReturnType -}) => ( - <> -

1. {firstStep}

- {firsStepDescription &&

{firsStepDescription}

} -

- Documentation:{' '} - - {documentation} - -

-

2. Export {secondStep}

- - -) - -export const AnsibleInstance = ({ - cluster, - state, - orgId, - goBack, - goBackToForm, - formStep, - setFormStep, -}: { - cluster?: boolean - state: useCloudProviderProps["initialState"] - orgId: number - goBack: () => void - goBackToForm: () => void - formStep: string - setFormStep: (step: string) => void -}) => { - const classes = formStyles() - const [orgKey, setOrgKey] = useState('') - const [isLoading, setIsLoading] = useState(false) - const [cloudImages, setCloudImages] = useState([]) - const [orgKeyError, setOrgKeyError] = useState(false) - - useEffect(() => { - setIsLoading(true) - getOrgKeys(orgId).then((data) => { - if (data.error !== null || !Array.isArray(data.response)) { - setIsLoading(false) - setOrgKeyError(true) - } else { - setOrgKeyError(false) - setOrgKey(data.response[0].value) - } - }) - getCloudImages({ - os_name: 'Ubuntu', - os_version: '22.04%20LTS', - arch: state.instanceType.arch, - cloud_provider: state.provider, - region: state.provider === 'aws' ? state.location.native_code : 'all', - }).then((data) => { - setIsLoading(false) - setOrgKeyError(false) - setCloudImages(data.response) - }) - }, [ - orgId, - state.instanceType.arch, - state.location.native_code, - state.provider, - ]) - - const AnsibleInstallation = () => ( - <> -

- 3. Install Ansible on your working machine (which could easily be a - laptop) -

-

example of installing the latest version:

- - - for more instructions on installing ansible, see{' '} - - here - - . - -

- 4. Clone the {cluster ? 'postgresql_cluster' : 'dle-se-ansible'}{' '} - repository -

- - - {!cluster && ( - <> -

5. Install requirements

- - - )} - - ) - - return ( - - {isLoading ? ( - - - - ) : ( - <> - {orgKeyError ? ( - - ) : state.provider === 'digitalocean' ? ( - - DO_API_TOKEN - - } - snippetContent="export DO_API_TOKEN=XXXXXX" - classes={classes} - /> - ) : state.provider === 'hetzner' ? ( - HCLOUD_API_TOKEN - } - snippetContent="export HCLOUD_API_TOKEN=XXXXXX" - classes={classes} - /> - ) : state.provider === 'aws' ? ( - - AWS_ACCESS_KEY_ID and - AWS_SECRET_ACCESS_KEY - - } - snippetContent={`export AWS_ACCESS_KEY_ID=XXXXXX\nexport AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXX`} - classes={classes} - /> - ) : state.provider === 'gcp' ? ( - - GCP_SERVICE_ACCOUNT_CONTENTS - - } - snippetContent={getGcpAccountContents()} - classes={classes} - /> - ) : null} - -

- {cluster - ? '5. Run ansible playbook to deploy Postgres Cluster' - : '6. Run ansible playbook to create server and install DBLab SE'} -

- - {getNetworkSubnet(state.provider, classes)} -

- {cluster - ? '6. After the code snippet runs successfully, follow the directions displayed in the resulting output to start using the database.' - : '7. After the code snippet runs successfully, follow the directions displayed in the resulting output to start using DBLab UI/API/CLI.'} -

- - - - - - )} -
- ) -} diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/DockerInstance.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/DockerInstance.tsx deleted file mode 100644 index d670be30..00000000 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/DockerInstance.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import { Button } from '@material-ui/core' -import { Box } from '@mui/material' -import { useEffect, useState } from 'react' - -import { ErrorStub } from '@postgres.ai/shared/components/ErrorStub' -import { Spinner } from '@postgres.ai/shared/components/Spinner' -import { SyntaxHighlight } from '@postgres.ai/shared/components/SyntaxHighlight' - -import { getCloudImages } from 'api/cloud/getCloudImages' -import { getOrgKeys } from 'api/cloud/getOrgKeys' - -import { - InstanceDocumentation, - formStyles, -} from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' -import { InstanceFormCreation } from 'components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation' -import { - getGcpAccountContents, - getNetworkSubnet, - getPlaybookCommand, -} from 'components/DbLabInstanceForm/utils' - -import { getClusterPlaybookCommand } from 'components/PostgresClusterForm/utils' -import { useCloudProviderProps } from 'hooks/useCloudProvider' - -export const DockerInstance = ({ - cluster, - state, - orgId, - goBack, - goBackToForm, - formStep, - setFormStep, -}: { - state: useCloudProviderProps['initialState'] - cluster?: boolean - orgId: number - goBack: () => void - goBackToForm: () => void - formStep: string - setFormStep: (step: string) => void -}) => { - const classes = formStyles() - const [orgKey, setOrgKey] = useState('') - const [isLoading, setIsLoading] = useState(false) - const [cloudImages, setCloudImages] = useState([]) - const [orgKeyError, setOrgKeyError] = useState(false) - - useEffect(() => { - setIsLoading(true) - getOrgKeys(orgId).then((data) => { - if (data.error !== null || !Array.isArray(data.response)) { - setIsLoading(false) - setOrgKeyError(true) - } else { - setOrgKeyError(false) - setOrgKey(data.response[0].value) - } - }) - getCloudImages({ - os_name: 'Ubuntu', - os_version: '22.04%20LTS', - arch: state.instanceType.arch, - cloud_provider: state.provider, - region: state.provider === 'aws' ? state.location.native_code : 'all', - }).then((data) => { - setIsLoading(false) - setOrgKeyError(false) - setCloudImages(data.response) - }) - }, [ - orgId, - state.instanceType.arch, - state.location.native_code, - state.provider, - ]) - - return ( - - {isLoading ? ( - - - - ) : ( - <> - {orgKeyError ? ( - - ) : state.provider === 'digitalocean' ? ( - DO_API_TOKEN} - snippetContent="export DO_API_TOKEN=XXXXXX" - classes={classes} - /> - ) : state.provider === 'hetzner' ? ( - HCLOUD_API_TOKEN - } - snippetContent="export HCLOUD_API_TOKEN=XXXXXX" - classes={classes} - /> - ) : state.provider === 'aws' ? ( - - AWS_ACCESS_KEY_ID and - AWS_SECRET_ACCESS_KEY - - } - snippetContent={`export AWS_ACCESS_KEY_ID=XXXXXX\nexport AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXX`} - classes={classes} - /> - ) : state.provider === 'gcp' ? ( - <> - - Create and save the JSON key for the service account and - point to them using{' '} - - GCP_SERVICE_ACCOUNT_CONTENTS - {' '} - variable. - - } - documentation="https://developers.google.com/identity/protocols/oauth2/service-account#creatinganaccount" - secondStep={ - - GCP_SERVICE_ACCOUNT_CONTENTS - - } - snippetContent={getGcpAccountContents()} - classes={classes} - /> - - ) : null} -

- 3. Run ansible playbook to{' '} - {cluster - ? 'deploy Postgres Cluster' - : 'create server and install DBLab SE'} -

- - {getNetworkSubnet(state.provider, classes)} -

- 4. After the code snippet runs successfully, follow the directions - displayed in the resulting output to start using{' '} - {cluster ? 'the database.' : 'DBLab UI/API/CLI.'} -

- - - - - - )} -
- ) -} diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation.tsx deleted file mode 100644 index 46cf4160..00000000 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import classNames from 'classnames' -import { makeStyles } from '@material-ui/core' - -const useStyles = makeStyles((theme) => ({ - snippetContainer: { - width: '100%', - height: '100%', - maxWidth: '800px', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - gap: 40, - - [theme.breakpoints.down('sm')]: { - flexDirection: 'column', - }, - - '& p:first-child': { - marginTop: '0', - }, - }, - fullWidth: { - width: '100%', - maxWidth: '100%', - - '& .MuiTextField-root': { - maxWidth: '800px', - } - }, - navigation: { - display: 'flex', - flexDirection: 'column', - marginLeft: '-20px', - flex: '0 0 220px', - - [theme.breakpoints.down('sm')]: { - flex: 'auto', - }, - - '& span': { - display: 'flex', - alignItems: 'center', - gap: 10, - cursor: 'pointer', - padding: '8px 14px 8px 20px', - borderBottom: '1px solid #CCD7DA', - transition: 'background-color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - - '&:hover': { - backgroundColor: '#F5F8FA', - }, - }, - }, - form: { - flex: '1 1 0', - overflow: 'auto', - - [theme.breakpoints.down('sm')]: { - flex: 'auto', - }, - }, - active: { - backgroundColor: '#F5F8FA', - borderRight: '4px solid #FF6212', - }, -})) - -export const InstanceFormCreation = ({ - formStep, - setFormStep, - children, - install, - fullWidth, -}: { - formStep: string - setFormStep: (step: string) => void - children: React.ReactNode - install?: boolean - fullWidth?: boolean -}) => { - const classes = useStyles() - - return ( -
-
- {!install && ( - setFormStep('simple')} - > - {'simple - Simple setup - - )} - setFormStep('docker')} - > - {'docker - Docker - - setFormStep('ansible')} - > - {'ansible - Ansible - -
-
{children}
-
- ) -} diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/SimpleInstance.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/SimpleInstance.tsx deleted file mode 100644 index 6b0e52d0..00000000 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/SimpleInstance.tsx +++ /dev/null @@ -1,587 +0,0 @@ -import { Button, TextField } from '@material-ui/core' -import { Box } from '@mui/material' -import { useCallback, useEffect, useState } from 'react' - -import { ErrorStub } from '@postgres.ai/shared/components/ErrorStub' -import { Spinner } from '@postgres.ai/shared/components/Spinner' -import { SyntaxHighlight } from '@postgres.ai/shared/components/SyntaxHighlight' -import { ResponseMessage } from '@postgres.ai/shared/pages/Configuration/ResponseMessage' -import { formStyles } from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' -import { InstanceFormCreation } from 'components/DbLabInstanceForm/DbLabFormSteps/InstanceFormCreation' - -import { useWsScroll } from '@postgres.ai/shared/pages/Logs/hooks/useWsScroll' -import { getCloudImages } from 'api/cloud/getCloudImages' -import { getOrgKeys } from 'api/cloud/getOrgKeys' -import { getTaskState } from 'api/configs/getTaskState' -import { launchDeploy } from 'api/configs/launchDeploy' -import { regenerateCode } from 'api/configs/regenerateCode' -import { useCloudProviderProps } from 'hooks/useCloudProvider' -import { cloudProviderName } from '../utils' -import { establishConnection } from './streamLogs' - -const SimpleInstanceDocumentation = ({ - state, - isLoading, - secondStep, - documentation, - deployingState, - handleDeploy, -}: { - isLoading: boolean - documentation: string - secondStep: JSX.Element - state: useCloudProviderProps['initialState'] - handleDeploy: (e: React.FormEvent) => void - deployingState: { - status: string - error: string - } -}) => { - const classes = formStyles() - - useEffect(() => { - const textFields = document.querySelectorAll('input[type="text"]') - textFields?.forEach((textField) => { - textField.addEventListener('blur', () => { - textField.setAttribute('type', 'password') - }) - textField.addEventListener('focus', () => { - textField.setAttribute('type', 'text') - }) - }) - }, []) - - return ( -
-

{cloudProviderName(state.provider)}

-

- {state.provider === 'aws' ? ( - <> - {`Create a ${cloudProviderName(state.provider)} access key per`}{' '} - - the official documentation. - {' '} - These secrets will be used securely in a - - ) : state.provider === 'gcp' ? ( - <> - {`Create a ${cloudProviderName( - state.provider, - )} service account per`}{' '} - - the official documentation. - {' '} - The service account content will be used securely in a - - ) : ( - <> - {`Generate a ${cloudProviderName(state.provider)} API token per`}{' '} - - the official documentation. - {' '} - This token will be used securely in a - - )}{' '} - - Postgres.ai - {' '} - temporary container and will not be stored. -

- {secondStep} -
- -
- - {deployingState.error && ( - - )} - - ) -} - -export const SimpleInstance = ({ - cluster, - state, - orgId, - userID, - goBackToForm, - formStep, - setFormStep, -}: { - cluster?: boolean - state: useCloudProviderProps['initialState'] - orgId: number - userID?: number - goBackToForm: () => void - formStep: string - setFormStep: (step: string) => void -}) => { - const classes = formStyles() - const hasTaskID = - new URLSearchParams(window.location.search).get('taskID') === state.taskID - const logElement = document.getElementById('logs-container') - const [orgKey, setOrgKey] = useState('') - const [isLoading, setIsLoading] = useState(false) - const [taskStatus, setTaskStatus] = useState('') - const [isConnected, setIsConnected] = useState(false) - const [deployingState, setDeployingState] = useState({ - status: 'stale', - error: '', - }) - useWsScroll(deployingState.status === 'loading', true) - const [cloudImages, setCloudImages] = useState([ - { - native_os_image: '', - }, - ]) - const [orgKeyError, setOrgKeyError] = useState(false) - - const [extraEnvs, setExtraEnvs] = useState({ - DO_API_TOKEN: '', - HCLOUD_API_TOKEN: '', - AWS_ACCESS_KEY_ID: '', - AWS_SECRET_ACCESS_KEY: '', - GCP_SERVICE_ACCOUNT_CONTENTS: '', - }) - - useEffect(() => { - if ( - state.provider && - state.location.native_code && - state.instanceType?.arch - ) { - setIsLoading(true) - getOrgKeys(orgId).then((data) => { - if (data.error !== null || !Array.isArray(data.response)) { - setIsLoading(false) - setOrgKeyError(true) - } else { - setOrgKeyError(false) - setOrgKey(data.response[0].value) - } - }) - getCloudImages({ - os_name: 'Ubuntu', - os_version: '22.04%20LTS', - arch: state.instanceType.arch, - cloud_provider: state.provider, - region: state.provider === 'aws' ? state.location.native_code : 'all', - }).then((data) => { - setIsLoading(false) - setOrgKeyError(false) - setCloudImages(data.response) - }) - } - }, [ - orgId, - state.instanceType?.arch, - state.location.native_code, - state.provider, - ]) - - useEffect(() => { - const handleHeightChange = () => { - if (logElement) { - logElement.scrollIntoView({ - behavior: 'smooth', - block: 'end', - }) - } - } - - const observer = new ResizeObserver(handleHeightChange) - if (logElement) { - observer.observe(logElement) - } - - return () => { - if (logElement) { - observer.unobserve(logElement) - } - } - }, [logElement]) - - const establishWebsocketConnection = useCallback( - ({ taskId, otCode }: { taskId: string; otCode: string }) => { - establishConnection({ - taskId: taskId, - otCode: otCode, - userID, - isConnected, - setIsConnected, - }).then(() => { - getTaskState({ taskID: taskId, userID }).then((status) => { - if (status.response) { - const responseStatus = - status.response?.state === 'error' || - status.response?.state === 'finished' - ? 'finished' - : 'loading' - setTaskStatus(responseStatus) - setDeployingState({ - status: 'finished', - error: '', - }) - } else if (status.error) { - setDeployingState({ - status: 'finished', - error: status.error?.Error, - }) - } - }) - }) - }, - [isConnected, userID], - ) - - useEffect(() => { - if ( - hasTaskID && - userID && - Object.values(extraEnvs).every((x) => x === null || x === '') && - taskStatus !== 'error' && - taskStatus !== 'finished' - ) { - setDeployingState({ - status: 'loading', - error: '', - }) - getTaskState({ taskID: state.taskID, userID }).then((data) => { - if (data.response?.state) { - regenerateCode({ taskID: state.taskID, userID }).then((res) => { - if (res.response) { - establishWebsocketConnection({ - taskId: state.taskID, - otCode: res.response.otCode, - }) - } else if (res.error) { - setDeployingState({ - status: 'finished', - error: res.error?.Error, - }) - } - }) - } else if (data.error) { - setDeployingState({ - status: 'finished', - error: data.error?.Error, - }) - } - }) - } - }, [ - hasTaskID, - state.taskID, - userID, - isConnected, - extraEnvs, - taskStatus, - establishWebsocketConnection, - ]) - - const handleDeploy = useCallback( - async (e: React.FormEvent) => { - e.preventDefault() - if (logElement) { - logElement.innerHTML = '' - } - - setDeployingState({ - status: 'loading', - error: '', - }) - await launchDeploy({ - launchType: cluster ? 'cluster' : 'instance', - state: state, - userID: userID, - extraEnvs: extraEnvs, - orgKey: orgKey, - cloudImage: cloudImages[0]?.native_os_image, - }) - .then(async (data) => { - if (data.response) { - window.history.pushState( - {}, - '', - `${window.location.pathname}?taskID=${data.response.taskID}&provider=${state.provider}`, - ) - establishWebsocketConnection({ - taskId: data.response.taskID, - otCode: data.response.otCode, - }) - setDeployingState({ - status: 'finished', - error: '', - }) - } else if (data.error) { - const error = - data.error.Error || - data.error.Errors[0] || - data.error.FieldErrors.playbook - - setDeployingState({ - status: 'stale', - error: error, - }) - if (logElement) { - logElement.innerHTML = error - } - } - }) - .catch(() => { - setDeployingState({ - ...deployingState, - status: 'stale', - }) - }) - }, - [ - state, - cluster, - extraEnvs, - orgKey, - cloudImages, - userID, - logElement, - deployingState, - establishWebsocketConnection, - ], - ) - - const isFormDisabled = - deployingState.status === 'loading' || - deployingState.status === 'finished' || - isConnected || - (hasTaskID && Object.values(extraEnvs).every((x) => x === null || x === '')) - - return ( - - {isLoading ? ( - - - - ) : ( - <> - {orgKeyError ? ( - - ) : state.provider === 'digitalocean' ? ( - - setExtraEnvs({ - ...extraEnvs, - DO_API_TOKEN: e.target.value, - }) - } - /> - } - /> - ) : state.provider === 'hetzner' ? ( - - setExtraEnvs({ - ...extraEnvs, - HCLOUD_API_TOKEN: e.target.value, - }) - } - /> - } - /> - ) : state.provider === 'aws' ? ( - - - setExtraEnvs({ - ...extraEnvs, - AWS_ACCESS_KEY_ID: e.target.value, - }) - } - /> - - setExtraEnvs({ - ...extraEnvs, - AWS_SECRET_ACCESS_KEY: e.target.value, - }) - } - /> - - } - /> - ) : state.provider === 'gcp' ? ( - { - e.target.style.color = 'transparent' - e.target.style.textShadow = '0 0 8px rgba(0,0,0,0.5)' - }} - onFocus={(e) => { - e.target.style.color = 'black' - e.target.style.textShadow = 'none' - }} - multiline - value={ - isFormDisabled - ? '****************' - : extraEnvs.GCP_SERVICE_ACCOUNT_CONTENTS - } - className={classes.marginTop} - InputLabelProps={{ - shrink: true, - }} - onChange={(e) => - setExtraEnvs({ - ...extraEnvs, - GCP_SERVICE_ACCOUNT_CONTENTS: e.target.value, - }) - } - /> - } - /> - ) : null} - {deployingState.status === 'loading' || - deployingState.status === 'finished' ? ( - - ) : null} - - - - - )} - - ) -} diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/streamLogs.ts b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/streamLogs.ts deleted file mode 100644 index 5cffaaff..00000000 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabFormSteps/streamLogs.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { initStreamLogs } from '@postgres.ai/platform/src/api/configs/initStreamLogs' -import { getTaskState } from 'api/configs/getTaskState' -import { regenerateCode } from 'api/configs/regenerateCode' - -export const establishConnection = async ({ - taskId, - otCode, - userID, - isConnected, - setIsConnected, -}: { - taskId: string - otCode: string - userID?: number - isConnected: boolean - setIsConnected: (isConnected: boolean) => void -}) => { - const logElement = document.getElementById('logs-container') - - if (logElement === null) { - return - } - - const appendLogElement = (logEntry: string) => { - const codeTag = logElement.querySelector('code') - if (codeTag) { - codeTag.appendChild(document.createTextNode(logEntry + '\n')) - logElement.appendChild(codeTag) - } - } - - const socket = initStreamLogs(taskId, otCode) - - socket.onclose = () => { - setIsConnected(false) - } - - socket.onerror = () => { - if (!isConnected) { - return - } - - setTimeout(() => { - getTaskState({ taskID: taskId, userID }).then((res) => { - if ( - res.response?.state && - res.response.state !== 'finished' && - res.response.state !== 'error' - ) { - while (logElement.firstChild) { - logElement.removeChild(logElement.firstChild) - } - regenerateCode({ taskID: taskId, userID }).then((res) => { - if (res.response) { - establishConnection({ - taskId, - otCode: res.response?.otCode, - userID, - isConnected, - setIsConnected, - }) - } - }) - } - }) - }, 5000) - } - - socket.onmessage = function (event) { - const logEntry = decodeURIComponent(atob(event.data)) - appendLogElement(logEntry) - setIsConnected(true) - } -} diff --git a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceForm.tsx b/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceForm.tsx deleted file mode 100644 index af7e76ec..00000000 --- a/ui/packages/platform/src/components/DbLabInstanceForm/DbLabInstanceForm.tsx +++ /dev/null @@ -1,584 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { - Button, - InputAdornment, - MenuItem, - Tab, - Tabs, - TextField, -} from '@material-ui/core' -import { Box } from '@mui/material' -import cn from 'classnames' - -import { ClassesType } from '@postgres.ai/platform/src/components/types' -import { Select } from '@postgres.ai/shared/components/Select' -import { Spinner } from '@postgres.ai/shared/components/Spinner' -import { StubSpinner } from '@postgres.ai/shared/components/StubSpinnerFlex' - -import { CloudInstance } from 'api/cloud/getCloudInstances' -import { CloudProvider } from 'api/cloud/getCloudProviders' -import { CloudRegion } from 'api/cloud/getCloudRegions' -import { CloudVolumes } from 'api/cloud/getCloudVolumes' -import { TabPanel } from 'pages/JoeSessionCommand/TabPanel' - -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { AnsibleInstance } from 'components/DbLabInstanceForm/DbLabFormSteps/AnsibleInstance' -import { DbLabInstanceFormSidebar } from 'components/DbLabInstanceForm/DbLabInstanceFormSidebar' -import { StorageSlider } from 'components/DbLabInstanceForm/DbLabInstanceFormSlider' -import { DbLabInstanceFormProps } from 'components/DbLabInstanceForm/DbLabInstanceFormWrapper' -import { initialState, reducer } from 'components/DbLabInstanceForm/reducer' -import { WarningWrapper } from 'components/Warning/WarningWrapper' -import { DockerInstance } from './DbLabFormSteps/DockerInstance' -import { SimpleInstance } from './DbLabFormSteps/SimpleInstance' - -import { - availableTags, - filteredRegions, - uniqueRegionsByProvider, -} from 'components/DbLabInstanceForm/utils' - -import urls from 'utils/urls' -import { validateDLEName } from 'utils/utils' - -import { useCloudProvider } from 'hooks/useCloudProvider' -import ConsolePageTitle from './../ConsolePageTitle' - -interface DbLabInstanceFormWithStylesProps extends DbLabInstanceFormProps { - classes: ClassesType - auth?: { - userId: number - } -} - -const DbLabInstanceForm = (props: DbLabInstanceFormWithStylesProps) => { - const { classes, orgPermissions } = props - const { - state, - dispatch, - handleChangeVolume, - handleGenerateToken, - handleReturnToForm, - handleSetFormStep, - } = useCloudProvider({ - initialState, - reducer, - }) - const permitted = !orgPermissions || orgPermissions.dblabInstanceCreate - - const pageTitle = - const breadcrumbs = ( - - ) - - const handleReturnToList = () => { - props.history.push(urls.linkDbLabInstances(props)) - } - - const requirePublicKeys = - !state.publicKeys && (state.provider === 'aws' || state.provider === 'gcp') - - const calculateVolumePrice = (databaseSize: number, snapshots: number) => { - let storage = databaseSize * snapshots - if (storage > 2000) storage = 2000 - - return (storage * state.volumePricePerHour) / 1000 - } - - if (state.isLoading) return - - return ( -
- {breadcrumbs} - - {pageTitle} - - {!permitted && ( - - You do not have permission to add Database Lab instances. - - )} - -
- {state.formStep === initialState.formStep && permitted ? ( - <> - {state.isReloading && ( - - )} -
-

- 1. Select your cloud provider -

-
- {state.serviceProviders.map( - (provider: CloudProvider, index: number) => ( -
- dispatch({ - type: 'change_provider', - provider: provider.api_name, - isReloading: true, - }) - } - > - {provider.label} -
- ), - )} -
-

- 2. Select your cloud region -

-
- | null, value: string) => - dispatch({ - type: 'change_region', - region: value, - location: state.cloudRegions.find( - (region: CloudRegion) => - region.world_part === value && - region.cloud_provider === state.provider, - ), - }) - } - > - {uniqueRegionsByProvider(state.cloudRegions).map( - (region: string, index: number) => ( - - ), - )} - -
- - {filteredRegions(state.cloudRegions, state.region).map( - (region: CloudRegion, index: number) => ( -
- dispatch({ - type: 'change_location', - location: region, - }) - } - > -

{region.api_name}

-

🏴 {region.label}

-
- ), - )} -
- {state.instanceType ? ( - <> -

- 3. Choose instance type -

-

- A larger instance can accommodate more dev/test activities. - For example, a team of 5 engineers requiring 5-10 clones - during peak times should consider a minimum instance size of - 8 vCPUs and 32 GiB. -

- - {state.cloudInstances.map( - (instance: CloudInstance, index: number) => ( -
- dispatch({ - type: 'change_instance_type', - instanceType: instance, - }) - } - > -

- {instance.api_name} ( - {state.instanceType.cloud_provider}:{' '} - {instance.native_name}) -

-
- 🔳 {instance.native_vcpus} CPU - 🧠 {instance.native_ram_gib} GiB RAM -
-
- ), - )} -
-

4. Database volume

- - - - - {(state.volumes as CloudVolumes[]).map((p, id) => { - const volumeName = `${p.api_name} (${p.cloud_provider}: ${p.native_name})` - return ( - - {volumeName} - - ) - })} - - - - - GiB - - ), - }} - value={Number(state.databaseSize)?.toFixed(2)} - className={classes.filterSelect} - onChange={( - event: React.ChangeEvent< - HTMLTextAreaElement | HTMLInputElement - >, - ) => { - dispatch({ - type: 'change_volume_price', - storage: Math.min( - Number(event.target.value) * state.snapshots, - 2000, - ), - databaseSize: event.target.value, - volumePrice: calculateVolumePrice( - Number(event.target.value), - state.snapshots, - ), - }) - }} - /> - × - - {Number(state.snapshots) === 1 - ? 'snapshot' - : 'snapshots'} - - ), - }} - value={state.snapshots} - className={classes.filterSelect} - onChange={( - event: React.ChangeEvent< - HTMLTextAreaElement | HTMLInputElement - >, - ) => { - dispatch({ - type: 'change_snapshots', - snapshots: Number(event.target.value), - storage: Math.min( - Number(event.target.value) * state.databaseSize, - 2000, - ), - volumePrice: calculateVolumePrice( - state.databaseSize, - Number(event.target.value), - ), - }) - }} - /> - - - , value: unknown) => { - dispatch({ - type: 'change_volume_price', - storage: value, - databaseSize: Number(value) / state.snapshots, - volumePrice: - (Number(value) * state.volumePricePerHour) / 1000, - }) - }} - /> - -

5. Provide DBLab name

- , - ) => - dispatch({ - type: 'change_name', - name: event.target.value, - }) - } - /> -

- 6. Define DBLab verification token (keep it secret!) -

-
- , - ) => - dispatch({ - type: 'change_verification_token', - verificationToken: event.target.value, - }) - } - /> - -
-

- 7. Choose DBLab version -

- { - const defaultTag = availableTags[0] - - return { - value: tag, - children: defaultTag === tag ? `${tag} (default)` : tag, - } - }) ?? [] - } - value={state.tag} - onChange={( - e: React.ChangeEvent, - ) => - dispatch({ - type: 'set_tag', - tag: e.target.value, - }) - } - /> -

- 4. Provide SSH public keys (one per line) -

{' '} -

- The specified ssh public keys will be added to authorized_keys - on the DBLab server. Add your public key here to have access to - the server after deployment. -

- , - ) => - dispatch({ - type: 'change_public_keys', - publicKeys: event.target.value, - }) - } - /> -
- - !validateDLEName(state.name) && handleSetFormStep('docker') - } - /> - - ) : state.formStep === 'ansible' && permitted ? ( - - ) : state.formStep === 'docker' && permitted ? ( - - ) : null} -
-
- ) -} - -export default DbLabInstanceInstallForm diff --git a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormSidebar.tsx b/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormSidebar.tsx deleted file mode 100644 index 44f601e9..00000000 --- a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormSidebar.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Button, makeStyles } from '@material-ui/core' -import { useCloudProviderProps } from 'hooks/useCloudProvider' - - -const useStyles = makeStyles({ - boxShadow: { - padding: '24px', - boxShadow: '0 8px 16px #3a3a441f, 0 16px 32px #5a5b6a1f', - }, - aside: { - width: '100%', - height: 'fit-content', - borderRadius: '4px', - display: 'flex', - flexDirection: 'column', - justifyContent: 'flex-start', - flex: '1 1 0', - position: 'sticky', - top: 10, - - '& h2': { - fontSize: '14px', - fontWeight: 500, - margin: '0 0 10px 0', - height: 'fit-content', - }, - - '& span': { - fontSize: '13px', - }, - - '& button': { - padding: '10px 20px', - marginTop: '20px', - }, - - '@media (max-width: 1200px)': { - position: 'relative', - boxShadow: 'none', - borderRadius: '0', - padding: '0', - flex: 'auto', - marginBottom: '30px', - - '& button': { - width: 'max-content', - }, - }, - }, - asideSection: { - padding: '12px 0', - borderBottom: '1px solid #e0e0e0', - - '& span': { - color: '#808080', - }, - - '& p': { - margin: '5px 0 0 0', - fontSize: '13px', - }, - }, -}) - -export const DbLabInstanceFormInstallSidebar = ({ - state, - handleCreate, - disabled, -}: { - state: useCloudProviderProps['initialState'] - handleCreate: () => void - disabled: boolean -}) => { - const classes = useStyles() - - return ( -
-
- {state.name && ( -
- Name -

{state.name}

-
- )} - {state.tag && ( -
- Tag -

{state.tag}

-
- )} - -
-
- ) -} diff --git a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormWrapper.tsx b/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormWrapper.tsx deleted file mode 100644 index 9e930bbf..00000000 --- a/ui/packages/platform/src/components/DbLabInstanceInstallForm/DbLabInstanceInstallFormWrapper.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { RouteComponentProps } from 'react-router' - -import DbLabInstanceInstallForm from 'components/DbLabInstanceInstallForm/DbLabInstanceInstallForm' - -import { useInstanceFormStyles } from 'components/DbLabInstanceForm/DbLabInstanceFormWrapper' -import { OrgPermissions } from 'components/types' - -export interface DbLabInstanceFormProps { - edit?: boolean - orgId: number - project: string | undefined - history: RouteComponentProps['history'] - orgPermissions: OrgPermissions -} - -export const DbLabInstanceFormInstallWrapper = ( - props: DbLabInstanceFormProps, -) => { - const classes = useInstanceFormStyles() - - return -} diff --git a/ui/packages/platform/src/components/DbLabInstanceInstallForm/reducer/index.tsx b/ui/packages/platform/src/components/DbLabInstanceInstallForm/reducer/index.tsx deleted file mode 100644 index 83a3fe76..00000000 --- a/ui/packages/platform/src/components/DbLabInstanceInstallForm/reducer/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ -import { ReducerAction } from 'react' - -import { availableTags } from 'components/DbLabInstanceForm/utils' - -export const initialState = { - isLoading: false, - formStep: 'create', - api_name: 'ssd', - name: '', - publicKeys: '', - tag: availableTags[0], - verificationToken: '', -} - -export const reducer = ( - state: typeof initialState, - // @ts-ignore - action: ReducerAction, -) => { - switch (action.type) { - case 'change_name': { - return { - ...state, - name: action.name, - } - } - case 'change_verification_token': { - return { - ...state, - verificationToken: action.verificationToken, - } - } - case 'change_public_keys': { - return { - ...state, - publicKeys: action.publicKeys, - } - } - case 'set_form_step': { - return { - ...state, - formStep: action.formStep, - } - } - case 'set_tag': { - return { - ...state, - tag: action.tag, - } - } - default: - throw new Error() - } -} diff --git a/ui/packages/platform/src/components/DbLabInstanceInstallForm/utils/index.ts b/ui/packages/platform/src/components/DbLabInstanceInstallForm/utils/index.ts deleted file mode 100644 index 513976d3..00000000 --- a/ui/packages/platform/src/components/DbLabInstanceInstallForm/utils/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { DEBUG_API_SERVER, sePackageTag } from 'components/DbLabInstanceForm/utils' -import { addIndentForDocker } from 'components/PostgresClusterInstallForm/utils' -import { useCloudProviderProps } from 'hooks/useCloudProvider' - -const API_SERVER = process.env.REACT_APP_API_SERVER - -export const getPlaybookCommand = ( - state: useCloudProviderProps['initialState'], - orgKey: string, - isDocker?: boolean, -) => { - const playbookCommand = `ansible-playbook deploy_dle.yml --extra-vars \\\r - "dblab_engine_host='user@server-ip-address' \\\r - platform_project_name='${state.name}' \\\r - dblab_engine_version='${state.tag}' \\\r - ${ orgKey ? `platform_org_key='${orgKey}' \\\r` : `` } - ${ API_SERVER === DEBUG_API_SERVER ? `platform_url='${DEBUG_API_SERVER}' \\\r` : `` } - ${ state.publicKeys ? `ssh_public_keys='${state.publicKeys}' \\\r` : `` } - dblab_engine_verification_token='${state.verificationToken}'"` - - if (isDocker) { - return `docker run --rm -it \\\r - --volume $HOME/.ssh:/root/.ssh:ro \\\r - --env ANSIBLE_SSH_ARGS="-F none" \\\r - postgresai/dle-se-ansible:${sePackageTag} \\\r - ${addIndentForDocker(playbookCommand)}` - } else { - return playbookCommand - } -} - -export const getAnsibleInstallationCommand = () => - `sudo apt update -sudo apt install -y python3-pip -pip3 install ansible` - -export const cloneRepositoryCommand = () => - `git clone --depth 1 --branch ${sePackageTag} \\ - https://gitlab.com/postgres-ai/dle-se-ansible.git \\ - && cd dle-se-ansible/` diff --git a/ui/packages/platform/src/components/DbLabInstances/DbLabInstances.tsx b/ui/packages/platform/src/components/DbLabInstances/DbLabInstances.tsx deleted file mode 100644 index f0fd8453..00000000 --- a/ui/packages/platform/src/components/DbLabInstances/DbLabInstances.tsx +++ /dev/null @@ -1,625 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component, MouseEvent } from 'react' -import { formatDistanceToNowStrict } from 'date-fns' -import { - Table, - TableBody, - TableCell, - TableHead, - TableRow, - TextField, - IconButton, - Menu, - MenuItem, - Tooltip, -} from '@material-ui/core' -import MoreVertIcon from '@material-ui/icons/MoreVert' -import WarningIcon from '@material-ui/icons/Warning' - -import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer' -import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner' -import { Spinner } from '@postgres.ai/shared/components/Spinner' -import { Modal } from '@postgres.ai/shared/components/Modal' -import { styles } from '@postgres.ai/shared/styles/styles' -import { - ClassesType, - ProjectProps, - RefluxTypes, -} from '@postgres.ai/platform/src/components/types' - -import Actions from '../../actions/actions' -import ConsolePageTitle from './../ConsolePageTitle' -import { ErrorWrapper } from 'components/Error/ErrorWrapper' -import { messages } from '../../assets/messages' -import Store from '../../stores/store' -import Urls from '../../utils/urls' -import format from '../../utils/format' -import { isHttps } from '../../utils/utils' -import { WarningWrapper } from 'components/Warning/WarningWrapper' -import { ProjectDataType, getProjectAliasById } from 'utils/aliases' -import { InstanceStateDto } from '@postgres.ai/shared/types/api/entities/instanceState' -import { InstanceDto } from '@postgres.ai/shared/types/api/entities/instance' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { DbLabStatusWrapper } from 'components/DbLabStatus/DbLabStatusWrapper' -import { DbLabInstancesProps } from 'components/DbLabInstances/DbLabInstancesWrapper' -import { CreatedDbLabCards } from 'components/CreateDbLabCards/CreateDbLabCards' -import { ConsoleButtonWrapper } from 'components/ConsoleButton/ConsoleButtonWrapper' - -interface DbLabInstancesWithStylesProps extends DbLabInstancesProps { - classes: ClassesType -} - -interface DbLabInstancesState { - modalOpen: boolean - data: { - auth: { - token: string - } | null - userProfile: { - data: { - orgs: ProjectDataType - } - } - dbLabInstances: { - orgId: number - data: { - [org: string]: { - created_at: string - project_label_or_name: string - plan: string - project_name: string - project_label: string - url: string - use_tunnel: boolean - isProcessing: boolean - id: string - project_alias: string - state: InstanceStateDto - dto: InstanceDto - } - } - isProcessing: boolean - projectId: string | number | undefined - error: boolean - } | null - dbLabInstanceStatus: { - instanceId: string - isProcessing: boolean - } - projects: Omit - } - anchorEl: (EventTarget & HTMLButtonElement) | null -} - -class DbLabInstances extends Component< - DbLabInstancesWithStylesProps, - DbLabInstancesState -> { - componentDidMount() { - const that = this - const orgId = this.props.orgId ? this.props.orgId : null - let projectId = this.props.projectId ? this.props.projectId : null - - if (!projectId) { - projectId = - this.props.match && - this.props.match.params && - this.props.match.params.projectId - ? this.props.match.params.projectId - : null - } - - if (projectId) { - Actions.setDbLabInstancesProject(orgId, projectId) - } else { - Actions.setDbLabInstancesProject(orgId, 0) - } - - this.unsubscribe = (Store.listen as RefluxTypes['listen'])(function () { - const auth: DbLabInstancesState['data']['auth'] = - this.data && this.data.auth ? this.data.auth : null - const dbLabInstances: DbLabInstancesState['data']['dbLabInstances'] = - this.data && this.data.dbLabInstances ? this.data.dbLabInstances : null - const projects: Omit = - this.data && this.data.projects ? this.data.projects : null - - if ( - auth && - auth.token && - !dbLabInstances?.isProcessing && - !dbLabInstances?.error && - !that.state - ) { - Actions.getDbLabInstances(auth.token, orgId, projectId) - } - - if ( - auth && - auth.token && - !projects.isProcessing && - !projects.error && - !that.state - ) { - Actions.getProjects(auth.token, orgId) - } - - that.setState({ data: this.data }) - }) - - Actions.refresh() - } - - unsubscribe: Function - componentWillUnmount() { - this.unsubscribe() - } - - handleClick = ( - _: MouseEvent, - id: string, - ) => { - const url = Urls.linkDbLabInstance(this.props, id) - - if (url) { - this.props.history.push(url) - } - } - - handleChangeProject = ( - event: React.ChangeEvent, - ) => { - const org = this.props.org ? this.props.org : null - const orgId = this.props.orgId ? this.props.orgId : null - const projectId = event.target.value - const project = this.state.data - ? getProjectAliasById(this.state.data?.userProfile?.data?.orgs, projectId) - : '' - const props = { org, orgId, projectId, project } - - Actions.setDbLabInstancesProject(orgId, event.target.value) - this.props.history.push(Urls.linkDbLabInstances(props)) - } - - registerButtonHandler = (provider: string) => { - this.props.history.push(Urls.linkDbLabInstanceAdd(this.props, provider)) - } - - openMenu = (event: MouseEvent) => { - event.stopPropagation() - this.setState({ anchorEl: event.currentTarget }) - } - - closeMenu = () => { - this.setState({ anchorEl: null }) - } - - menuHandler = (_: MouseEvent, action: string) => { - const anchorEl = this.state.anchorEl - - this.closeMenu() - - setTimeout(() => { - const auth = - this.state.data && this.state.data.auth ? this.state.data.auth : null - const data = - this.state.data && this.state.data.dbLabInstances - ? this.state.data.dbLabInstances - : null - - if (anchorEl) { - const instanceId = anchorEl.getAttribute('aria-label') - if (!instanceId) { - return - } - - let project = '' - if (data?.data) { - for (const i in data.data) { - if (parseInt(data.data[i].id, 10) === parseInt(instanceId, 10)) { - project = data.data[i].project_alias - } - } - } - - switch (action) { - case 'addclone': - this.props.history.push( - Urls.linkDbLabCloneAdd( - { org: this.props.org, project: project }, - instanceId, - ), - ) - - break - - case 'destroy': - /* eslint no-alert: 0 */ - if ( - window.confirm( - 'Are you sure you want to remove this Database Lab instance?', - ) === true - ) { - Actions.destroyDbLabInstance(auth?.token, instanceId) - } - - break - - case 'refresh': - Actions.getDbLabInstanceStatus(auth?.token, instanceId) - - break - - case 'editProject': - this.props.history.push( - Urls.linkDbLabInstanceEditProject( - { org: this.props.org, project: project }, - instanceId, - ), - ) - - break - - default: - break - } - } - }, 100) - } - - render() { - const { classes, orgPermissions, orgId } = this.props - const data = - this.state && this.state.data && this.state.data.dbLabInstances - ? this.state.data.dbLabInstances - : null - const projects = - this.state && this.state.data && this.state.data?.projects - ? this.state.data.projects - : null - const projectId = this.props.projectId ? this.props.projectId : null - const menuOpen = Boolean(this.state && this.state.anchorEl) - const title = 'Database Lab Instances' - const addPermitted = !orgPermissions || orgPermissions.dblabInstanceCreate - const deletePermitted = - !orgPermissions || orgPermissions.dblabInstanceDelete - - const getVersionDigits = (str: string) => { - if (!str) { - return 'N/A' - } - - const digits = str.match(/\d+/g) - - if (digits && digits.length > 0) { - return `${digits[0]}.${digits[1]}.${digits[2]}` - } - return '' - } - - const addInstanceButton = ( - this.setState({ modalOpen: true })} - title={addPermitted ? 'Create new DBLab' : messages.noPermission} - > - New DBLab - - ) - const pageTitle = ( - 0 - ? [addInstanceButton] - : [] - } - /> - ) - - let projectFilter = null - if (projects && projects.data && data) { - projectFilter = ( -
- this.handleChangeProject(event)} - select - label="Project" - inputProps={{ - name: 'project', - id: 'project-filter', - }} - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - variant="outlined" - className={classes.filterSelect} - > - All - - {projects.data.map((p) => { - return ( - - {p?.project_label_or_name || p.name} - - ) - })} - -
- ) - } - - const breadcrumbs = ( - - ) - - if (orgPermissions && !orgPermissions.dblabInstanceList) { - return ( -
- {breadcrumbs} - - {pageTitle} - - {messages.noPermissionPage} -
- ) - } - - if (this.state?.data && this.state.data?.dbLabInstances?.error) { - return ( -
- {breadcrumbs} - - - - -
- ) - } - - if ( - !data || - (data && data.isProcessing) || - data.orgId !== orgId || - data.projectId !== (projectId ? projectId : 0) - ) { - return ( -
- {breadcrumbs} - - - - -
- ) - } - - const CardsModal = () => ( - this.setState({ modalOpen: false })} - aria-labelledby="simple-modal-title" - aria-describedby="simple-modal-description" - > - - - ) - - let table = ( - - ) - - let menu = null - if (data.data && Object.keys(data.data).length > 0) { - table = ( - - - - - Project - Instance ID - URL - Clones - Plan - Version - State - Created at -   - - - - - {Object.keys(data.data).map((index) => { - return ( - - this.handleClick(event, data.data[index].id) - } - style={{ cursor: 'pointer' }} - > - - {data.data[index].project_label_or_name || - data.data[index].project_name} - - - - {data.data[index].id} - - - {data.data[index].state && data.data[index].url - ? data.data[index].url - : 'N/A'} - {!isHttps(data.data[index].url) && - data.data[index].url && - !data.data[index].use_tunnel ? ( - - - - ) : null} - - - {data.data[index]?.state?.cloning?.numClones ?? - data.data[index]?.state?.clones?.length ?? - 'N/A'} - - - {data.data[index] && - (data.data[index]?.plan === 'EE' - ? 'Enterprise' - : data.data[index]?.plan === 'SE' - ? 'Standard' - : data.data[index]?.plan)} - - - {getVersionDigits( - data.data[index] && - (data.data[index].state?.engine?.version as string), - )} - - - {data.data[index].state && data.data[index].url ? ( - - ) : ( - 'N/A' - )} - - - - - {format.formatTimestampUtc( - data.data[index].created_at, - ) ?? ''} - - - - - {data.data[index].isProcessing || - (this.state.data?.dbLabInstanceStatus.instanceId === - index && - this.state.data.dbLabInstanceStatus.isProcessing) ? ( - - ) : null} - - - - - - ) - })} - -
-
- ) - - const selectedInstance = Object.values(data.data).filter((item) => { - const anchorElLabel = this.state.anchorEl?.getAttribute('aria-label') - // eslint-disable-next-line eqeqeq - return anchorElLabel && item.id == anchorElLabel - })[0] - - menu = ( - - this.menuHandler(event, 'editProject')} - disabled={!addPermitted || selectedInstance?.plan === 'SE'} - > - Edit - - this.menuHandler(event, 'addclone')} - disabled={selectedInstance?.plan === 'SE'} - > - Create clone - - this.menuHandler(event, 'refresh')} - disabled={selectedInstance?.plan === 'SE'} - > - Refresh - - this.menuHandler(event, 'destroy')} - > - Remove from List - - - ) - } - - return ( -
- {breadcrumbs} - - {pageTitle} - - {projectFilter} - - {table} - - {menu} - - -
- ) - } -} - -export default DbLabInstances diff --git a/ui/packages/platform/src/components/DbLabInstances/DbLabInstancesWrapper.tsx b/ui/packages/platform/src/components/DbLabInstances/DbLabInstancesWrapper.tsx deleted file mode 100644 index 0b75f20f..00000000 --- a/ui/packages/platform/src/components/DbLabInstances/DbLabInstancesWrapper.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import { styles } from '@postgres.ai/shared/styles/styles' -import DbLabInstances from 'components/DbLabInstances/DbLabInstances' -import { RouteComponentProps } from 'react-router' -import { colors } from '@postgres.ai/shared/styles/colors' -import { OrgPermissions } from 'components/types' - -export interface DbLabInstancesProps { - orgId: number - org: string | number - project: string | undefined - projectId: string | number | undefined - history: RouteComponentProps['history'] - match: { - params: { - project?: string - projectId?: string | number | undefined - org?: string - } - } - orgPermissions: OrgPermissions -} - -export const DbLabInstancesWrapper = (props: DbLabInstancesProps) => { - const useStyles = makeStyles( - { - root: { - ...(styles.root as Object), - display: 'flex', - flexDirection: 'column', - }, - filterSelect: { - ...styles.inputField, - width: 150, - }, - cell: { - '& > a': { - color: 'black', - textDecoration: 'none', - }, - '& > a:hover': { - color: 'black', - textDecoration: 'none', - }, - }, - inTableProgress: { - width: '30px!important', - height: '30px!important', - }, - warningIcon: { - color: colors.state.warning, - fontSize: '1.2em', - position: 'absolute', - marginLeft: 5, - }, - tooltip: { - fontSize: '10px!important', - }, - timeLabel: { - lineHeight: '16px', - fontSize: 12, - cursor: 'pointer', - }, - buttonContainer: { - display: 'flex', - gap: 10, - }, - flexContainer: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - height: '100%', - gap: 40, - marginTop: '20px', - - '& > div': { - maxWidth: '300px', - width: '100%', - height: '100%', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - border: '1px solid #e0e0e0', - padding: '20px', - borderRadius: '4px', - cursor: 'pointer', - fontSize: '15px', - transition: 'border 0.3s ease-in-out', - - '&:hover': { - border: '1px solid #FF6212', - }, - }, - }, - }, - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/DbLabSession/DbLabSession.tsx b/ui/packages/platform/src/components/DbLabSession/DbLabSession.tsx deleted file mode 100644 index e41e63f7..00000000 --- a/ui/packages/platform/src/components/DbLabSession/DbLabSession.tsx +++ /dev/null @@ -1,1018 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component } from 'react' -import { NavLink } from 'react-router-dom' -import { - Button, - Table, - TableBody, - TableCell, - TableRow, - TextField, - ExpansionPanelSummary, - ExpansionPanelDetails, - ExpansionPanel, - Typography, -} from '@material-ui/core' -import ExpandMoreIcon from '@material-ui/icons/ExpandMore' -import { formatDistanceToNowStrict } from 'date-fns' - -import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer' -import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner' -import { Spinner } from '@postgres.ai/shared/components/Spinner' -import { icons } from '@postgres.ai/shared/styles/icons' -import { ClassesType, RefluxTypes } from '@postgres.ai/platform/src/components/types' - -import Store from '../../stores/store' -import Actions from '../../actions/actions' -import { ErrorWrapper } from 'components/Error/ErrorWrapper' -import ConsolePageTitle from './../ConsolePageTitle' -import { WarningWrapper } from 'components/Warning/WarningWrapper' -import { messages } from '../../assets/messages' -import format from '../../utils/format' -import urls, { PropsType } from '../../utils/urls' -import { GatewayLink } from '@postgres.ai/shared/components/GatewayLink' -import dblabutils from '../../utils/dblabutils' -import { MomentInput } from 'moment' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { DbLabStatusWrapper } from 'components/DbLabStatus/DbLabStatusWrapper' -import { DbLabSessionProps } from 'components/DbLabSession/DbLabSessionWrapper' - -interface DbLabSessionWithStylesProps extends DbLabSessionProps { - classes: ClassesType -} - -interface Session { - id: number - project_name: string - internal_instance_id: string | undefined - duration: number - started_at: string - config: { - [config: string | number]: number - } - tags: { - request_link: string - username_full: string - username_link: string - launched_by: string - dle_version: string - data_state_at: string - branch: string - branch_link: string - revision: string - revision_link: string - } - result: { - status: string - intervals: { - warning: boolean - started_at: MomentInput - duration: number - }[] - summary: { - elapsed: boolean - total_intervals: number - total_duration: number - checklist: { - [x: string]: { author_id: string } - } - } - } -} - -interface DbLabSessionState { - logsExpanded: boolean - artifactsExpanded: { [x: string]: boolean } - configurationExpanded: boolean - intervalsExpanded: boolean - data: { - dbLabInstanceStatus: { - error: boolean - } - auth: { - token: string | null - } | null - dbLabSession: { - artifactsData: Record - isLogsComplete: boolean - isArtifactsProcessed: boolean - isArtifactsProcessing: boolean - isLogDownloading: boolean - downloadingArtifactType: string - isArtifactDownloading: boolean - artifacts: - | { - artifact_size: number - artifact_type: string - dblab_session_id: string - }[] - | null - artifactData: { - [data: string]: { - isProcessing: boolean - isProcessed: boolean - data: string - } - } | null - logs: { - process_id: string - connection_from: string - session_id: string - session_line_num: string - command_tag: string - session_start_time: string - virtual_transaction_id: string - transaction_id: string - error_severity: string - sql_state_code: string - message: string - detail: string - hint: string - internal_query: string - internal_query_pos: string - context: string - query: string - query_pos: string - location: string - application_name: string - backend_type: string - database_name: string - user_name: string - log_time: string - id: number - }[] - error: boolean - errorMessage: string - errorCode: number - isLogsProcessing: boolean - isProcessing: boolean - data: Session - } | null - } | null -} - -const PAGE_SIZE = 20 - -class DbLabSession extends Component< - DbLabSessionWithStylesProps, - DbLabSessionState -> { - unsubscribe: Function - componentDidMount() { - const sessionId = this.props.match.params.sessionId - const that = this - - this.unsubscribe = (Store.listen as RefluxTypes["listen"]) (function () { - const auth = this.data && this.data.auth ? this.data.auth : null - const dbLabSession = - this.data && this.data.dbLabSession ? this.data.dbLabSession : null - - if (auth && auth.token && !dbLabSession?.isProcessing && !that.state) { - Actions.getDbLabSession(auth.token, sessionId) - } - - if ( - auth && - auth.token && - !dbLabSession?.isLogsProcessing && - !that.state - ) { - Actions.getDbLabSessionLogs(auth.token, { sessionId, limit: PAGE_SIZE }) - } - - if ( - auth && - auth.token && - !dbLabSession?.isArtifactsProcessing && - !that.state - ) { - Actions.getDbLabSessionArtifacts(auth.token, sessionId) - } - - that.setState({ data: this.data }) - - const contentContainer = document.getElementById('logs-container') - if (contentContainer && !contentContainer.getAttribute('onscroll')) { - contentContainer.addEventListener('scroll', () => { - if ( - contentContainer.scrollTop >= - contentContainer.scrollHeight - contentContainer.offsetHeight - ) { - this.showMore() - } - }) - contentContainer.setAttribute('onscroll', '1') - } - }) - - Actions.refresh() - } - - componentWillUnmount() { - this.unsubscribe() - } - - showMore() { - const sessionId = this.props.match.params.sessionId - const auth = - this.state.data && this.state.data.auth ? this.state.data.auth : null - const session = - this.state.data && this.state.data.dbLabSession - ? this.state.data.dbLabSession - : null - let lastId = null - - if (session && session.logs && session.logs.length) { - lastId = session.logs[session.logs.length - 1].id - } - - if (auth && auth.token && !session?.isLogsProcessing && lastId) { - Actions.getDbLabSessionLogs(auth.token, { - sessionId, - limit: PAGE_SIZE, - lastId, - }) - } - } - - getCheckDetails(session: Session, check: string) { - let intervals = null - let maxIntervals = null - let totalDuration = null - let maxDuration = null - switch (check) { - case 'no_long_dangerous_locks': - if ( - session && - session.result && - session.result.summary && - session.result.summary.total_intervals - ) { - intervals = session.result.summary.total_intervals - } - if (session && session.config && session.config.observation_interval) { - maxIntervals = session.config.observation_interval - } - if (intervals && maxIntervals) { - return ( - '(' + - intervals + - ' ' + - (intervals > 1 ? 'intervals' : 'interval') + - ' with locks of ' + - maxIntervals + - ' allowed)' - ) - } - break - case 'session_duration_acceptable': - if ( - session && - session.result && - session.result.summary && - session.result.summary.total_duration - ) { - totalDuration = session.result.summary.total_duration - } - if (session && session.config && session.config.max_duration) { - maxDuration = session.config.max_duration - } - if (totalDuration && maxDuration) { - return ( - '(spent ' + - format.formatSeconds(totalDuration, 0, '') + - ' of the allowed ' + - format.formatSeconds(maxDuration, 0, '') + - ')' - ) - } - break - - default: - } - - return '' - } - - downloadLog = () => { - const auth = - this.state && this.state.data && this.state.data.auth - ? this.state.data.auth - : null - const sessionId = this.props.match.params.sessionId - - Actions.downloadDblabSessionLog(auth?.token, sessionId) - } - - downloadArtifact = (artifactType: string) => { - const auth = - this.state && this.state.data && this.state.data.auth - ? this.state.data.auth - : null - const sessionId = this.props.match.params.sessionId - - Actions.downloadDblabSessionArtifact(auth?.token, sessionId, artifactType) - } - - getArtifact = (artifactType: string) => { - const auth = - this.state && this.state.data && this.state.data.auth - ? this.state.data.auth - : null - const sessionId = this.props.match.params.sessionId - - Actions.getDbLabSessionArtifact(auth?.token, sessionId, artifactType) - } - - render() { - const that = this - const { classes, orgPermissions } = this.props - const sessionId = this.props.match.params.sessionId - const data = - this.state && this.state.data && this.state.data.dbLabSession - ? this.state.data.dbLabSession - : null - const title = 'Database Lab observed session #' + sessionId - - const pageTitle = - const breadcrumbs = ( - - ) - - if (orgPermissions && !orgPermissions.dblabSessionView) { - return ( -
- {breadcrumbs} - - {pageTitle} - - {messages.noPermissionPage} -
- ) - } - - let errorWidget = null - if (this.state && this.state.data?.dbLabSession?.error) { - errorWidget = ( - - ) - } - - if ( - this.state && - (this.state.data?.dbLabSession?.error || - this.state.data?.dbLabInstanceStatus?.error) - ) { - return ( -
- {breadcrumbs} - - {pageTitle} - - {errorWidget} -
- ) - } - - if ( - !data || - (this.state && - this.state.data && - this.state.data?.dbLabSession?.isProcessing) - ) { - return ( -
- {breadcrumbs} - - {pageTitle} - - -
- ) - } - - const session = data && data.data ? data.data : null - const logs = data && data.logs ? data.logs : null - const artifacts = data && data.artifacts ? data.artifacts : null - const artifactData = data && data.artifactData ? data.artifactData : null - - return ( -
- {breadcrumbs} - - {pageTitle} - -
-
Summary
- - - Status: - - - -
- - - Session: - {session ? '#' + session.id : '-'} - - - - Project: - {session?.project_name ? session.project_name : '-'} - - - - DBLab instance: - {session?.internal_instance_id ? ( - - {'#' + session.internal_instance_id} - - ) : ( - '' - )} - - - - DBLab version: - {session && session.tags && session.tags.dle_version - ? session.tags.dle_version - : '-'} - - -
- - - Data state at: - {session && session.tags && session.tags.data_state_at - ? session.tags.data_state_at - : '-'} - - -
- - - Duration: - {session && - session.result && - session.result.summary && - session.result.summary.elapsed - ? session.result.summary.elapsed - : null} - {!( - session && - session.result && - session.result.summary && - session.result.summary.elapsed - ) && - session && - session.duration > 0 - ? format.formatSeconds(session.duration, 0, '') - : null} - - - - Created: - {session && - formatDistanceToNowStrict(new Date(session.started_at), { - addSuffix: true, - })} - - -
- - - Branch: - {session && - session.tags && - session.tags.branch && - session.tags.branch_link ? ( - - {session.tags.branch} - - ) : ( - - {session && session.tags && session.tags.branch - ? session.tags.branch - : '-'} - - )} - - - - Commit: - {session && - session.tags && - session.tags.revision && - session.tags.revision_link ? ( - - {session.tags.revision} - - ) : ( - - {session && session.tags && session.tags.revision - ? session.tags.revision - : '-'} - - )} - - - - Triggered by: - {session && - session.tags && - session.tags.launched_by && - session.tags.username_link ? ( - - {session.tags.launched_by} - {session.tags.username_full - ? ' (' + session.tags.username_full + ')' - : ''} - - ) : ( - - {session && session.tags && session.tags.launched_by - ? session.tags.launched_by - : '-'} - - )} - - - - PR/MR: - {session && session.tags && session.tags.request_link ? ( - - {session.tags.request_link} - - ) : ( - '-' - )} - - - - Changes: - {session && session.tags && session.tags.request_link ? ( - - {session.tags.request_link} - - ) : ( - '-' - )} - - - {false && ( - - Check documentation for the details about observed sessions: - - Database Lab – CI Observer - - - )} -
- -
- -
-
Checklist
- - {session?.result && - session.result.summary && - session.result.summary.checklist ? ( -
- {Object.keys(session.result.summary.checklist).map(function ( - key, - ) { - const details = that.getCheckDetails(session, key) - return ( - -
- {session.result.summary?.checklist && - session.result.summary.checklist[key] ? ( - - ) : ( - - )} -
-
- {format.formatDbLabSessionCheck(key)} -
{details}
-
-
- ) - })} -
- ) : ( - icons.processingLargeIcon - )} -
- -
- -
-
- Observed intervals and details -
- { - that.setState({ intervalsExpanded: expanded }) - }} - > - } - aria-controls="panel1b-content" - id="panel1b-header" - className={classes.expansionPanelSummary} - > - {that.state.intervalsExpanded - ? 'Hide intervals' - : 'Show intervals'} - - - {session?.result && - session.result?.intervals && - session.result.intervals.length > 0 ? ( -
-
-
-
Started at
-
Duration
-
- {session.result.intervals.map((i) => { - return ( -
-
-
- {i.warning - ? icons.intervalWarning - : icons.intervalOk} -
-
- {format.formatTimestampUtc(i.started_at)} -
-
- {format.formatSeconds(i.duration, 0, '')} -
-
- {i.warning && ( -
-
- -
-
- )} -
- ) - })} -
- ) : ( - - Not specified - - )} - - -
- -
- -
-
Configuration
- - { - that.setState({ configurationExpanded: expanded }) - }} - > - } - aria-controls="panel1b-content" - id="panel1b-header" - className={classes.expansionPanelSummary} - > - {that.state.configurationExpanded - ? 'Hide configuration' - : 'Show configuration'} - - - {session?.config ? ( -
- {Object.keys(session.config).map(function (key) { - return ( - - {key}: - {session.config && session.config[key]} - - ) - })} -
- ) : ( - - Not specified - - )} -
-
-
- -
- -
- -
-
- Artifacts -
- - {Array.isArray(artifacts) && artifacts.length ? ( - - - - {artifacts.map((a) => { - return ( - { - if ( - orgPermissions && - !orgPermissions.dblabSessionArtifactsView - ) { - return - } - const artifactsExpanded = - that.state.artifactsExpanded || {} - artifactsExpanded[a.artifact_type] = - !artifactsExpanded[a.artifact_type] - that.setState({ - artifactsExpanded: artifactsExpanded, - }) - if ( - artifactsExpanded[a.artifact_type] && - a.artifact_type !== 'log' && - (!data.artifactsData || - (data.artifactsData && - !data.artifactsData[a.artifact_type])) - ) { - this.getArtifact(a.artifact_type) - } - }} - > - -
-
- {a.artifact_type}  - {orgPermissions && - orgPermissions.dblabSessionArtifactsView && ( - - {icons.detailsArrow} - - )} -
-
- {dblabutils.getArtifactDescription( - a.artifact_type, - )} -
-
- {format.formatBytes(a.artifact_size, 0, true)} -
- {orgPermissions && - orgPermissions.dblabSessionArtifactsView && ( -
- -
- )} -
-
- - } - aria-controls="panel1b-content" - id="panel1b-header" - className={ - classes.artifactExpansionPanelSummary - } - > - {that.state.logsExpanded - ? 'Hide log' - : 'Show log'} - - - {a.artifact_type === 'log' ? ( -
- {Array.isArray(logs) && logs.length ? ( -
- {logs.map((r) => { - return ( -
- {r.log_time},{r.user_name}, - {r.database_name},{r.process_id}, - {r.connection_from},{r.session_id} - ,{r.session_line_num}, - {r.command_tag}, - {r.session_start_time}, - {r.virtual_transaction_id}, - {r.transaction_id}, - {r.error_severity}, - {r.sql_state_code},{r.message}, - {r.detail},{r.hint}, - {r.internal_query}, - {r.internal_query_pos},{r.context} - ,{r.query},{r.query_pos}, - {r.location},{r.application_name}, - {r.backend_type} -
- ) - })} -
- {data && data.isLogsProcessing && ( - - )} - {data && - !data.isLogsProcessing && - !data.isLogsComplete && ( - - )} -
-
- ) : ( - 'No log uploaded yet.' - )} -
- ) : ( -
- {artifactData && - artifactData[a.artifact_type] && - artifactData[a.artifact_type] - .isProcessing ? ( - - ) : null} - {artifactData && - artifactData[a.artifact_type] && - artifactData[a.artifact_type].isProcessed && - artifactData[a.artifact_type].data ? ( -
- {artifactData[a.artifact_type].data} -
- ) : null} -
- )} -
-
-
-
-
- ) - })} -
-
-
- ) : ( - - {data.isArtifactsProcessed ? 'Artifacts not found' : ''} - {data.isArtifactsProcessing ? ( - - ) : null} - - )} -
- -
-
- ) - } -} - -export default DbLabSession diff --git a/ui/packages/platform/src/components/DbLabSession/DbLabSessionWrapper.tsx b/ui/packages/platform/src/components/DbLabSession/DbLabSessionWrapper.tsx deleted file mode 100644 index 781de3b8..00000000 --- a/ui/packages/platform/src/components/DbLabSession/DbLabSessionWrapper.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import { colors } from '@postgres.ai/shared/styles/colors' -import { styles } from '@postgres.ai/shared/styles/styles' -import DbLabSession from 'components/DbLabSession/DbLabSession' -import { OrgPermissions } from 'components/types' -import { RouteComponentProps } from 'react-router' - -interface MatchParams { - sessionId: string -} - -export interface DbLabSessionProps extends RouteComponentProps { - orgPermissions: OrgPermissions -} - -export interface ErrorProps { - code?: number - message?: string -} - -export const DbLabSessionWrapper = (props: DbLabSessionProps) => { - const useStyles = makeStyles( - (theme) => ({ - root: { - ...(styles.root as Object), - flex: '1 1 100%', - display: 'flex', - flexDirection: 'column', - }, - summary: { - marginTop: 20, - marginBottom: 20, - }, - paramTitle: { - display: 'inline-block', - width: 200, - }, - sectionHeader: { - fontWeight: 600, - display: 'block', - paddingBottom: 10, - marginBottom: 10, - borderBottom: '1px solid ' + colors.consoleStroke, - }, - logContainer: { - backgroundColor: 'black', - color: 'white', - fontFamily: - '"Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas",' + - ' "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace', - fontSize: '13px', - maxHeight: 'calc(100vh - 120px)', - overflowY: 'auto', - width: '100%', - '& > div': { - overflowWrap: 'anywhere', - }, - }, - artifactContainer: { - backgroundColor: 'black', - color: 'white', - fontFamily: - '"Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas",' + - ' "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace', - fontSize: '13px', - maxHeight: 'calc(100vh - 300px)', - width: '100%', - whiteSpace: 'break-spaces', - overflowWrap: 'anywhere', - overflow: 'auto', - }, - showMoreContainer: { - marginTop: 20, - textAlign: 'center', - }, - link: { - color: colors.secondary2.main, - '&:visited': { - color: colors.secondary2.main, - }, - '&:hover': { - color: colors.secondary2.main, - }, - '&:active': { - color: colors.secondary2.main, - }, - }, - checkStatusColumn: { - display: 'block', - width: 80, - marginTop: 10, - height: 30, - float: 'left', - }, - checkDescriptionColumn: { - display: 'inline-block', - }, - checkDetails: { - clear: 'both', - display: 'block', - color: colors.pgaiDarkGray, - }, - checkListItem: { - marginBottom: 10, - minHeight: 30, - }, - cfgListItem: { - marginBottom: 5, - }, - expansionPanel: { - marginTop: '5px!important', - borderRadius: '0px!important', - }, - expansionPanelSummary: { - display: 'inline-block', - padding: '5px', - paddingLeft: '12px', - minHeight: '30px', - lineHeight: '30px', - width: '100%', - '& .MuiExpansionPanelSummary-content': { - margin: '0px', - display: 'inline-block', - }, - '&.Mui-expanded': { - minHeight: '22px', - }, - '& .MuiExpansionPanelSummary-expandIcon': { - display: 'inline-block', - padding: '0px', - marginTop: '-1px', - }, - }, - expansionPanelDetails: { - padding: '12px', - paddingTop: '0px', - [theme.breakpoints.down('md')]: { - display: 'block', - }, - }, - intervalsRow: { - borderBottom: '1px solid ' + colors.consoleStroke, - width: '100%', - lineHeight: '22px', - '&:last-child': { - borderBottom: 'none', - }, - }, - intervalIcon: { - display: 'inline-block', - width: 25, - }, - intervalStarted: { - display: 'inline-block', - width: 200, - }, - intervalDuration: { - display: 'inline-block', - }, - intervalWarning: { - display: 'inline-block', - width: '100%', - }, - code: { - width: '100%', - 'margin-top': 0, - '& > div': { - paddingTop: 8, - padding: 8, - }, - 'background-color': 'rgb(246, 248, 250)', - '& > div > textarea': { - fontFamily: - '"Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas",' + - ' "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace', - color: 'black', - fontSize: '12px', - }, - }, - button: { - marginTop: 5, - marginBottom: 10, - }, - bottomSpace: { - ...styles.bottomSpace, - }, - artifactRow: { - padding: '5px', - cursor: 'pointer', - [theme.breakpoints.down('sm')]: { - paddingLeft: '0px', - paddingRight: '0px', - paddingTop: '10px', - }, - }, - artifactName: { - display: 'inline-block', - width: '20%', - [theme.breakpoints.down('sm')]: { - display: 'block', - width: '100%', - marginBottom: '10px', - }, - '& svg': { - verticalAlign: 'middle', - }, - }, - artifactDescription: { - display: 'inline-block', - width: '40%', - [theme.breakpoints.down('sm')]: { - display: 'block', - width: '100%', - marginBottom: '10px', - }, - }, - artifactSize: { - display: 'inline-block', - width: '20%', - [theme.breakpoints.down('sm')]: { - display: 'block', - width: '100%', - marginBottom: '10px', - }, - }, - artifactAction: { - display: 'inline-block', - width: '20%', - textAlign: 'right', - '& button': { - marginBottom: '5px', - }, - [theme.breakpoints.down('sm')]: { - display: 'block', - width: '100%', - }, - }, - artifactExpansionPanel: { - padding: '0px!important', - boxShadow: 'none', - }, - artifactExpansionPanelSummary: { - display: 'none', - minHeight: '0px!important', - }, - artifactsExpansionPanelDetails: { - padding: '0px!important', - }, - summaryDivider: { - minHeight: '10px', - }, - rotate180Icon: { - '& svg': { - transform: 'rotate(180deg)', - }, - }, - rotate0Icon: { - '& svg': { - transform: 'rotate(0deg)', - }, - }, - }), - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/DbLabSessions/DbLabSessions.tsx b/ui/packages/platform/src/components/DbLabSessions/DbLabSessions.tsx deleted file mode 100644 index fdbbc1a6..00000000 --- a/ui/packages/platform/src/components/DbLabSessions/DbLabSessions.tsx +++ /dev/null @@ -1,404 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component, MouseEvent } from 'react' -import { RouteComponentProps } from 'react-router' -import { - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Button, -} from '@material-ui/core' -import { formatDistanceToNowStrict } from 'date-fns' - -import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer' -import { StubContainer } from '@postgres.ai/shared/components/StubContainer' -import { PageSpinner } from '@postgres.ai/shared/components/PageSpinner' -import { Spinner } from '@postgres.ai/shared/components/Spinner' -import { icons } from '@postgres.ai/shared/styles/icons' -import { ClassesType, RefluxTypes } from '@postgres.ai/platform/src/components/types' - -import Store from '../../stores/store' -import Actions from '../../actions/actions' -import { ErrorWrapper } from 'components/Error/ErrorWrapper' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' -import { ProductCardWrapper } from 'components/ProductCard/ProductCardWrapper' -import { DbLabStatusWrapper } from 'components/DbLabStatus/DbLabStatusWrapper' - -import ConsolePageTitle from './../ConsolePageTitle' -import format from '../../utils/format' - -interface DbLabSessionsProps { - classes: ClassesType - org: string | number - orgId: number - history: RouteComponentProps['history'] -} - -interface DbLabSessionsState { - data: { - auth: { - token: string - } | null - dbLabSessions: { - isProcessing: boolean - isProcessed: boolean - isComplete: boolean - error: boolean - data: { - id: number - duration: number - started_at: string - tags: { - instance_id: string - branch: string - revision: string - launched_by: string - project_id: number - } - result: { - status: string - summary: { - checklist: { [list: string]: { string: string | boolean } } - elapsed: string - } - } - }[] - } | null - } | null -} - -const PAGE_SIZE = 20 - -class DbLabSessions extends Component { - unsubscribe: Function - componentDidMount() { - const that = this - const { orgId } = this.props - - this.unsubscribe = (Store.listen as RefluxTypes["listen"]) (function () { - const auth = this.data && this.data.auth ? this.data.auth : null - const sessions = - this.data && this.data.dbLabSessions ? this.data.dbLabSessions : null - - if ( - auth && - auth.token && - !sessions?.isProcessing && - !sessions?.error && - !that.state - ) { - Actions.getDbLabSessions(auth.token, { orgId, limit: PAGE_SIZE }) - } - - that.setState({ data: this.data }) - }) - - const contentContainer = document.getElementById('content-container') - if (contentContainer) { - contentContainer.addEventListener('scroll', () => { - if ( - contentContainer.scrollTop >= - contentContainer.scrollHeight - contentContainer.offsetHeight - ) { - this.showMore() - } - }) - } - - Actions.refresh() - } - - componentWillUnmount() { - this.unsubscribe() - } - - onSessionClick( - _: MouseEvent, - sessionId: string | number, - ) { - const { org } = this.props - - this.props.history.push('/' + org + '/observed-sessions/' + sessionId) - } - - formatStatus(status: string) { - const { classes } = this.props - let icon = null - let className = null - let label = status - if (status.length) { - label = status.charAt(0).toUpperCase() + status.slice(1) - } - - switch (status) { - case 'passed': - icon = icons.okIcon - className = classes.passedStatus - break - case 'failed': - icon = icons.failedIcon - className = classes.failedStatus - break - default: - icon = icons.processingIcon - className = classes.processingStatus - } - - return ( -
- - {icon} {label} - -
- ) - } - - showMore() { - const { orgId } = this.props - const auth = - this.state.data && this.state.data.auth ? this.state.data.auth : null - const sessions = - this.state.data && this.state.data.dbLabSessions - ? this.state.data.dbLabSessions - : null - let lastId = null - - if (sessions && sessions.data && sessions.data.length) { - lastId = sessions.data[sessions.data.length - 1].id - } - - if (auth && auth.token && !sessions?.isProcessing && lastId) { - Actions.getDbLabSessions(auth.token, { - orgId, - limit: PAGE_SIZE, - lastId, - }) - } - } - - render() { - const { classes, org } = this.props - - const breadcrumbs = ( - - ) - - const pageTitle = ( - - ) - - if (!this.state || !this.state.data) { - return ( -
- {breadcrumbs} - {pageTitle} - - -
- ) - } - - const sessionsStore = - (this.state.data && this.state.data.dbLabSessions) || null - const sessions = (sessionsStore && sessionsStore.data) || [] - - if (sessionsStore && sessionsStore.error) { - return ( -
- {breadcrumbs} - - {pageTitle} - - -
- ) - } - - if (!sessionsStore || !sessionsStore.data) { - return ( -
- {breadcrumbs} - - {pageTitle} - - -
- ) - } - - return ( -
- {breadcrumbs} - {pageTitle} - - {sessions && sessions.length > 0 ? ( -
- - - - Status - Session - - Project/Instance - - Commit - Checklist -   - - - - {sessions.map((s) => { - if (s) { - return ( - { - this.onSessionClick(event, s.id) - return false - }} - style={{ cursor: 'pointer' }} - > - - - - - #{s.id} - - - {s.tags && s.tags.project_id - ? s.tags.project_id - : '-'} - / - {s.tags && s.tags.instance_id - ? s.tags.instance_id - : '-'} - - - {icons.branch}  - {s.tags && s.tags.branch && s.tags.revision - ? s.tags.branch + '/' + s.tags.revision - : '-'} - - - {s.result && - s.result.summary && - s.result.summary.checklist ? ( -
- {Object.keys(s.result.summary.checklist).map( - function (key) { - return ( - - {s.result?.summary?.checklist && - s.result.summary.checklist[key] - ? icons.okLargeIcon - : icons.failedLargeIcon} -   - - ) - }, - )} -
- ) : ( - icons.processingLargeIcon - )} -
- -
- {s.duration > 0 || - (s.result && - s.result.summary && - s.result.summary.elapsed) ? ( - - {icons.timer}  - {s.result && - s.result.summary && - s.result.summary.elapsed - ? s.result.summary.elapsed - : format.formatSeconds(s.duration, 0, '')} - - ) : ( - '-' - )} -
-
- {icons.calendar} created  - {formatDistanceToNowStrict( - new Date(s.started_at), - { addSuffix: true }, - )} - {s.tags && s.tags.launched_by ? ( - by {s.tags.launched_by} - ) : ( - '' - )} -
-
-
- ) - } - - return null - })} -
-
-
-
- {sessionsStore && sessionsStore.isProcessing && ( - - )} - {sessionsStore && - !sessionsStore.isProcessing && - !sessionsStore.isComplete && ( - - )} -
-
- ) : ( - <> - {sessions && sessions.length === 0 && sessionsStore.isProcessed && ( - - - - )} - - )} -
- ) - } -} - -export default DbLabSessions diff --git a/ui/packages/platform/src/components/DbLabSessions/DbLabSessionsWrapper.tsx b/ui/packages/platform/src/components/DbLabSessions/DbLabSessionsWrapper.tsx deleted file mode 100644 index 9eae282a..00000000 --- a/ui/packages/platform/src/components/DbLabSessions/DbLabSessionsWrapper.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import { styles } from '@postgres.ai/shared/styles/styles' -import { RouteComponentProps } from 'react-router' -import DbLabSessions from 'components/DbLabSessions/DbLabSessions' - -interface DbLabSessionsProps { - org: string | number - orgId: number - history: RouteComponentProps['history'] -} - -export const DbLabSessionsWrapper = (props: DbLabSessionsProps) => { - const useStyles = makeStyles( - { - root: { - ...(styles.root as Object), - paddingBottom: '20px', - display: 'flex', - flexDirection: 'column', - }, - tableHead: { - ...(styles.tableHead as Object), - textAlign: 'left', - }, - tableCell: { - textAlign: 'left', - }, - showMoreContainer: { - marginTop: 20, - textAlign: 'center', - }, - }, - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/DbLabStatus/DbLabStatus.tsx b/ui/packages/platform/src/components/DbLabStatus/DbLabStatus.tsx deleted file mode 100644 index ac1d51e9..00000000 --- a/ui/packages/platform/src/components/DbLabStatus/DbLabStatus.tsx +++ /dev/null @@ -1,216 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component } from 'react' -import Brightness1Icon from '@material-ui/icons/Brightness1' -import Tooltip from '@material-ui/core/Tooltip' - -import { icons } from '@postgres.ai/shared/styles/icons' -import { ClassesType } from '@postgres.ai/platform/src/components/types' - -import Format from '../../utils/format' -import { Clone } from '@postgres.ai/shared/types/api/entities/clone' -import { DbLabStatusProps } from 'components/DbLabStatus/DbLabStatusWrapper' - -export interface DbLabStatusInstance { - state: { - status: { - code: string - message: string - } - } -} -interface DbLabStatusWithStylesProps extends DbLabStatusProps { - classes: ClassesType -} - -class DbLabStatus extends Component { - getCloneStatus = ( - clone: Clone, - onlyText: boolean, - showDescription: boolean, - ) => { - const { classes } = this.props - let className = classes?.cloneReadyStatus - - if (!clone.status) { - return null - } - - switch (clone.status.code) { - case 'OK': - className = classes?.cloneReadyStatus - break - case 'CREATING': - className = classes?.cloneCreatingStatus - break - case 'DELETING': - className = classes?.cloneDeletingStatus - break - case 'RESETTING': - className = classes?.cloneResettingStatus - break - case 'FATAL': - className = classes?.cloneFatalStatus - break - default: - break - } - - if (onlyText && showDescription) { - return ( - - - -   - {Format.formatStatus(clone.status.code)} - - - {clone.status.message && clone.status.message.length > 100 ? ( - - {Format.limitStr(clone.status.message, 100)} - - ) : ( - clone.status.message - )} - - - ) - } - - if (onlyText && !showDescription) { - return ( - - - - -   - {Format.formatStatus(clone.status.code)} - - ) - } - - return ( - - - - ) - } - - getInstanceStatus = (instance: DbLabStatusInstance, onlyText: boolean) => { - const { classes } = this.props - let className = classes?.instanceReadyStatus - - if (!instance.state) { - return null - } - - if (!instance.state.status) { - return null - } - switch (instance.state.status.code) { - case 'OK': - className = classes?.instanceReadyStatus - break - case 'WARNING': - className = classes?.instanceWarningStatus - break - case 'NO_RESPONSE': - className = classes?.instanceNoResponseStatus - break - default: - break - } - - if (onlyText) { - return ( - - - - -   - {Format.formatStatus(instance.state.status.code)} - - ) - } - - return ( - - - - ) - } - - getSessionStatus = (session: { status: string }) => { - const { classes } = this.props - let icon = null - let className = null - let label = session.status - if (session.status.length) { - label = session.status.charAt(0).toUpperCase() + session.status.slice(1) - } - - switch (session.status) { - case 'passed': - icon = icons.okIconWhite - className = classes?.sessionPassedStatus - break - case 'failed': - icon = icons.failedIconWhite - className = classes?.sessionFailedStatus - break - default: - icon = icons.processingIconWhite - className = classes?.sessionProcessingStatus - } - - return ( -
- - {icon} - {label} - -
- ) - } - - render() { - const { onlyText, showDescription, instance, clone, session } = this.props - - if (clone) { - return this.getCloneStatus( - clone, - onlyText as boolean, - showDescription as boolean, - ) - } - - if (instance) { - return this.getInstanceStatus(instance, onlyText as boolean) - } - - if (session) { - return this.getSessionStatus(session) - } - - return null - } -} - -export default DbLabStatus diff --git a/ui/packages/platform/src/components/DbLabStatus/DbLabStatusWrapper.tsx b/ui/packages/platform/src/components/DbLabStatus/DbLabStatusWrapper.tsx deleted file mode 100644 index b4c20e89..00000000 --- a/ui/packages/platform/src/components/DbLabStatus/DbLabStatusWrapper.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import DbLabStatus, { - DbLabStatusInstance, -} from 'components/DbLabStatus/DbLabStatus' -import { colors } from '@postgres.ai/shared/styles/colors' -import { Clone } from '@postgres.ai/shared/types/api/entities/clone' - -export interface DbLabStatusProps { - session?: { status: string } - onlyText?: boolean - showDescription?: boolean - instance?: DbLabStatusInstance - clone?: Clone -} - -export const DbLabStatusWrapper = (props: DbLabStatusProps) => { - const useStyles = makeStyles( - { - cloneReadyStatus: { - color: colors.state.ok, - fontSize: '1.1em', - verticalAlign: 'middle', - '& svg': { - marginTop: '-3px', - }, - }, - cloneCreatingStatus: { - color: colors.state.processing, - fontSize: '1.1em', - verticalAlign: 'middle', - '& svg': { - marginTop: '-3px', - }, - }, - cloneResettingStatus: { - color: colors.state.processing, - fontSize: '1.1em', - verticalAlign: 'middle', - '& svg': { - marginTop: '-3px', - }, - }, - cloneDeletingStatus: { - color: colors.state.warning, - fontSize: '1.1em', - verticalAlign: 'middle', - '& svg': { - marginTop: '-3px', - }, - }, - cloneFatalStatus: { - color: colors.state.error, - fontSize: '1.1em', - verticalAlign: 'middle', - '& svg': { - marginTop: '-3px', - }, - }, - instanceReadyStatus: { - color: colors.state.ok, - fontSize: '1.1em', - verticalAlign: 'middle', - '& svg': { - marginTop: '-3px', - }, - }, - instanceWarningStatus: { - color: colors.state.warning, - fontSize: '1.1em', - verticalAlign: 'middle', - '& svg': { - marginTop: '-3px', - }, - }, - instanceNoResponseStatus: { - color: colors.state.error, - fontSize: '1.1em', - verticalAlign: 'middle', - '& svg': { - marginTop: '-3px', - }, - }, - toolTip: { - fontSize: '10px!important', - }, - sessionPassedStatus: { - display: 'inline-block', - border: '1px solid ' + colors.state.ok, - fontSize: '12px', - color: '#FFFFFF', - backgroundColor: colors.state.ok, - padding: '3px', - paddingLeft: '5px', - paddingRight: '5px', - borderRadius: 3, - lineHeight: '14px', - '& svg': { - width: 10, - height: 10, - marginBottom: '-1px', - marginRight: '5px', - }, - }, - sessionFailedStatus: { - display: 'inline-block', - border: '1px solid ' + colors.state.error, - fontSize: '12px', - color: '#FFFFFF', - backgroundColor: colors.state.error, - padding: '3px', - paddingLeft: '5px', - paddingRight: '5px', - borderRadius: 3, - lineHeight: '14px', - '& svg': { - width: 10, - height: 10, - marginBottom: '-1px', - marginRight: '5px', - }, - }, - sessionProcessingStatus: { - display: 'inline-block', - border: '1px solid ' + colors.state.processing, - fontSize: '12px', - color: '#FFFFFF', - backgroundColor: colors.state.processing, - padding: '3px', - paddingLeft: '5px', - paddingRight: '5px', - borderRadius: 3, - lineHeight: '14px', - '& svg': { - width: 10, - height: 10, - marginBottom: '-1px', - marginRight: '5px', - }, - }, - }, - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/DisplayToken/DisplayToken.tsx b/ui/packages/platform/src/components/DisplayToken/DisplayToken.tsx deleted file mode 100644 index 8c08e09f..00000000 --- a/ui/packages/platform/src/components/DisplayToken/DisplayToken.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component } from 'react' -import { InputAdornment } from '@material-ui/core' -import { IconButton, TextField } from '@material-ui/core' - -import { ClassesType, RefluxTypes } from '@postgres.ai/platform/src/components/types' -import { styles } from '@postgres.ai/shared/styles/styles' -import { icons } from '@postgres.ai/shared/styles/icons' - -import Store from '../../stores/store' -import Actions from '../../actions/actions' - -interface DisplayTokenProps { - classes: ClassesType -} - -interface DisplayTokenState { - data: { - tokenRequest: { - isProcessed: boolean - error: boolean - data: { - name: string - expires_at: string - token: string - } - } | null - } | null -} - -class DisplayToken extends Component { - unsubscribe: Function - componentDidMount() { - const that = this - - document.getElementsByTagName('html')[0].style.overflow = 'hidden' - - this.unsubscribe = (Store.listen as RefluxTypes["listen"]) (function () { - that.setState({ data: this.data }) - }) - - Actions.refresh() - } - - componentWillUnmount() { - this.unsubscribe() - } - - copyToken = () => { - const copyText = document.getElementById( - 'generatedToken', - ) as HTMLInputElement - - if (copyText) { - copyText.select() - copyText.setSelectionRange(0, 99999) - document.execCommand('copy') - } - } - - render() { - const { classes } = this.props - const tokenRequest = - this.state && this.state.data && this.state.data.tokenRequest - ? this.state.data.tokenRequest - : null - let tokenDisplay = null - - if ( - tokenRequest && - tokenRequest.isProcessed && - !tokenRequest.error && - tokenRequest.data && - tokenRequest.data.name && - tokenRequest.data.expires_at && - tokenRequest.data.token - ) { - tokenDisplay = ( - - - {icons.copyIcon} - - - ), - }} - InputLabelProps={{ - shrink: true, - style: styles.inputFieldLabel, - }} - FormHelperTextProps={{ - style: styles.inputFieldHelper, - }} - helperText="Make sure you have saved token - you will not be able to access it again" - /> - ) - } - - return
{tokenDisplay}
- } -} - -export default DisplayToken diff --git a/ui/packages/platform/src/components/DisplayToken/DisplayTokenWrapper.tsx b/ui/packages/platform/src/components/DisplayToken/DisplayTokenWrapper.tsx deleted file mode 100644 index 87328243..00000000 --- a/ui/packages/platform/src/components/DisplayToken/DisplayTokenWrapper.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import DisplayToken from 'components/DisplayToken/DisplayToken' -import { styles } from '@postgres.ai/shared/styles/styles' - -export const DisplayTokenWrapper = () => { - const useStyles = makeStyles( - { - textField: { - ...styles.inputField, - marginTop: 0, - }, - input: { - '&.MuiOutlinedInput-adornedEnd': { - padding: 0, - }, - }, - inputElement: { - marginRight: '-8px', - }, - inputAdornment: { - margin: 0, - }, - inputButton: { - padding: '9px 10px', - }, - }, - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/Error/Error.tsx b/ui/packages/platform/src/components/Error/Error.tsx deleted file mode 100644 index 23815d41..00000000 --- a/ui/packages/platform/src/components/Error/Error.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { Component } from 'react' -import { Paper, Typography } from '@material-ui/core' - -import { ClassesType } from '@postgres.ai/platform/src/components/types' -import { ErrorProps } from 'components/Error/ErrorWrapper' - -interface ErrorWithStylesProps extends ErrorProps { - classes: ClassesType -} - -class Error extends Component { - render() { - const { classes } = this.props - - return ( -
- - - ERROR {this.props.code ? this.props.code : null} - - -
- - - {this.props.message - ? this.props.message - : 'Unknown error occurred. Please try again later.'} - -
-
- ) - } -} - -export default Error diff --git a/ui/packages/platform/src/components/Error/ErrorWrapper.tsx b/ui/packages/platform/src/components/Error/ErrorWrapper.tsx deleted file mode 100644 index 6e840000..00000000 --- a/ui/packages/platform/src/components/Error/ErrorWrapper.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { makeStyles } from '@material-ui/core' -import Error from 'components/Error/Error' - -export interface ErrorProps { - code?: number - message?: string -} - -export const ErrorWrapper = (props: ErrorProps) => { - const useStyles = makeStyles( - (theme) => ({ - paper: theme.mixins.gutters({ - paddingTop: 16, - paddingBottom: 16, - marginTop: 0, - }), - errorHead: { - color: '#c00111', - fontWeight: 'bold', - fontSize: '16px', - }, - errorText: { - color: '#c00111', - }, - }), - { index: 1 }, - ) - - const classes = useStyles() - - return -} diff --git a/ui/packages/platform/src/components/ExplainVisualization/ExplainVisualization.tsx b/ui/packages/platform/src/components/ExplainVisualization/ExplainVisualization.tsx deleted file mode 100644 index b001c3a5..00000000 --- a/ui/packages/platform/src/components/ExplainVisualization/ExplainVisualization.tsx +++ /dev/null @@ -1,295 +0,0 @@ -/*-------------------------------------------------------------------------- - * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai - * All Rights Reserved. Proprietary and confidential. - * Unauthorized copying of this file, via any medium is strictly prohibited - *-------------------------------------------------------------------------- - */ - -import { - AppBar, - Dialog, - Button, - IconButton, - TextField, - Toolbar, - Typography, -} from '@material-ui/core' -import CloseIcon from '@material-ui/icons/Close' -import React, { Component } from 'react' -import Store from '../../stores/store' - -import { styles } from '@postgres.ai/shared/styles/styles' -import { Spinner } from '@postgres.ai/shared/components/Spinner' -import { ClassesType, RefluxTypes } from '@postgres.ai/platform/src/components/types' - -import Actions from '../../actions/actions' -import { ConsoleBreadcrumbsWrapper } from 'components/ConsoleBreadcrumbs/ConsoleBreadcrumbsWrapper' - -import ConsolePageTitle from '../ConsolePageTitle' -import FlameGraph from '../FlameGraph' -import explainSamples from '../../assets/explainSamples' -import { visualizeTypes } from '../../assets/visualizeTypes' - -interface ExplainVisualizationProps { - classes: ClassesType -} - -interface ExplainVisualizationState { - plan: string | null - showFlameGraph: boolean - data: { - externalVisualization: { - url: string - type: string - isProcessing: boolean - } - } | null -} - -class ExplainVisualization extends Component< - ExplainVisualizationProps, - ExplainVisualizationState -> { - unsubscribe: Function - componentDidMount() { - const that = this - - this.unsubscribe = (Store.listen as RefluxTypes["listen"]) (function () { - that.setState({ data: this.data }) - }) - - Actions.refresh() - } - - componentWillUnmount() { - this.unsubscribe() - } - - handleChange = (event: React.ChangeEvent) => { - this.setState({ - plan: event.target.value, - }) - } - - insertSample = () => { - this.setState({ plan: explainSamples[0].value }) - } - - getExternalVisualization = () => { - return this.state && - this.state.data && - this.state.data.externalVisualization - ? this.state.data.externalVisualization - : null - } - - showExternalVisualization = (type: string) => { - const { plan } = this.state - - if (!plan) { - return - } - - Actions.getExternalVisualizationData(type, plan, '') - } - - closeExternalVisualization = () => { - Actions.closeExternalVisualization() - this.setState({ - showFlameGraph: false, - }) - } - - handleExternalVisualizationClick = (type: string) => { - return () => { - this.showExternalVisualization(type) - } - } - - showFlameGraphVisualization = () => { - this.setState({ - showFlameGraph: true, - }) - } - - render() { - const { classes } = this.props - - const breadcrumbs = ( - - ) - - const pageTitle = ( - -

- Visualize explain plans gathered manually. Plans gathered with Joe - will be automatically saved in Joe history and can be visualized - in command page without copy-pasting of a plan. -

-

Currently only JSON format is supported.

-

- For better results, use: explain (analyze, costs, verbose, - buffers, format json). -

- - } - /> - ) - - if (!this.state || !this.state.data) { - return ( -
- {breadcrumbs} - - {pageTitle} - - -
- ) - } - - const { plan, showFlameGraph } = this.state - - const externalVisualization = this.getExternalVisualization() - - const disableVizButtons = - !plan || showFlameGraph || externalVisualization?.isProcessing - const openVizDialog = - showFlameGraph || - (externalVisualization?.url - ? externalVisualization.url?.length > 0 - : false) - - return ( -
- {breadcrumbs} - - {pageTitle} - -
- -
- - - -
- - - - - -
- -
- - - - - Visualization - - - - - - - - {showFlameGraph && ( -
-

Flame Graph (buffers):

- - -

Flame Graph (timing):

- -
- )} - - {externalVisualization?.url && ( -

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 - - - - - - - - - diff --git a/ui/packages/platform/public/images/docker.svg b/ui/packages/platform/public/images/docker.svg deleted file mode 100644 index 2dd944c7..00000000 --- a/ui/packages/platform/public/images/docker.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/ui/packages/platform/public/images/globe.svg b/ui/packages/platform/public/images/globe.svg deleted file mode 100644 index f2f0671c..00000000 --- a/ui/packages/platform/public/images/globe.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/ui/packages/platform/public/images/infosrc.png b/ui/packages/platform/public/images/infosrc.png deleted file mode 100644 index 244c86ab377d1bff3e52918901062035f03d85e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1109 zcmV-b1giUqP)eZ#2fdzjexusm=b+ zB>C(JzYUBb?)~f9AasVFeApC@oII#=gp^73n3RST%=a3 z0nqRFiJm;+)^zOChZ`IYA{=2f+7W;Nf*`nJ_fnT~sAx8u6bc11u(q~FrBdPO=qR1@ z(XTJy3Zv0Z0b@>hshn`77<2yomJx3Nq9|f>a}zy{gk~w>(J;gTmc|H!TMt|*B>?9#%N1cB zzYP?}rUCL_1> zbWS@agVUhk`bNmCT`U$Ul}Z?ZIF8L6caLDNAF#CezY{!bZI2N!_cZ9C5fJ8*%`@0H zLgq7T1o5_hWHksPnaf99ub)vPB=WqCN=_RgdHyTQY#X8MH^JOCLM%oZoSy=#GH5Kv zMbOe{b%INg0-2RT&PMQE1{cl)0+PX)nT> btXT0cF4bVK-9gX@00000NkvXXu0mjfyzcm` diff --git a/ui/packages/platform/public/images/oauth-github-logo.png b/ui/packages/platform/public/images/oauth-github-logo.png deleted file mode 100644 index 1fa19c55d2f71505edf0f4d70840e817e7861c06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1151 zcmV-_1c3XAP)aRaZbWef~;-qbQN~T3m5@KGy*SRcU1Y@_%gT?qoIV1 z#+_K&r^Z*p_=X zkV~rhX7~W|+y`hDck^w~4$6pjXcK4iU7?KWf;Lf`Z;tj*Mzu%tC~tV5KpFQ5a$y3v z3i_Y8%G&()sD>)o4V&Xsyo>oz!sg>$oQln{8&p9xws&p52?j$IbSrY-#c&;_K{1<- z>#$gnal1hk3`P@|0B(UCJXEH}Vt4|z5QNG24X@!|T!~8nPIDPv!*7@j0o0%t7ArH} z!;phpDmA|b7C;V8bNtGD5>^fI9<(Y>!j=xVJq>cO0Be*h^gQHfKaXal`$3MLFI8j} zEP@;z>`?%RK#mq+m10F6feg;@D1b8{gGY+Z?I%J8&v+ETGmybVG|De<5Mhug4BpHyFaa{S*~3NIn;?S;|J-qX$Y3m*dKf@cjDZZ+4>&jn zGPuFxrTH5mgL4879)%3n^f-XEAcIE(4!(gLjSf8Dj4y+40uBa2j(*HQ06#*G1_c~U zh8(?>fdJly98C$FXMezJ83^Dt$kBqpy1tfy06v9W83M@6d%#cD0OmoChGZasA&{ea zfpHdCjb`NuhCv2* zgk20d&cgtX3$-Z1!7`B1P&D_b`Q{h~87&iKN$Dkz0(dFXlAis20KiQiG=B>e!X5cV z)`LQN0Zr@aGyerBr1c65c7Q?}goBYw>5p26V+a&dhhl|Kfr|JUJL6EiP+|7i3zuTG zgjRd5LO&>?Q;H4H5Tl?1qp?BW?bdPJiQ#w+SK$yeF*Mr>N8={EgE3G{MxkM;fzO4C zn2VG1ZnrcBLylfY&iFKc1xmoVkv=*)|djts1F)e8hB5r zgarjJhg+RiUQL_*j}HcWx(@t2RKhm}Pt|*%G7gLo!2VE7p0^C#vckvACIz?O7H?uO zW?(2j!YODHA%HcY7z{y6>);z<4phYZDSg>l42ozDHVV4~N0fP^J3Ipc)Z&P!J8=$F z#5^47c>w3cwIi26C47kOkn=2nOXA*{lPfGZPR6hJ3}2uFW?-?D1E|KyNj)WZ#YD&; z%>X81*QDQ=R>ZF!2JkCZOlvNl!-bgRK>%}bF>>{>8nXgkK^;`WauMn;4V6%bSFwT* zYjPXlbu5AkEFGclQc!_vyp9cg{9$4RT!#S=-ie{+I}iroI{ZE=#`q{01pu+n%tOqP Rd2;{&002ovPDHLkV1i^k^r8R& diff --git a/ui/packages/platform/public/images/oauth-gitlab-logo.png b/ui/packages/platform/public/images/oauth-gitlab-logo.png deleted file mode 100644 index 7ab02d6a4b8002bb506f71ee94b7ecd3f25a5b51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8464 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 Xcf1=#ExaOi00000NkvXXu0mjfwHywk diff --git a/ui/packages/platform/public/images/oauth-linkedin-logo.png b/ui/packages/platform/public/images/oauth-linkedin-logo.png deleted file mode 100644 index 9086cb15f4ac308ebc229cac879cf5e5bf1c637c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6132 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 diff --git a/ui/packages/platform/public/images/paymentMethods/unionpay.png b/ui/packages/platform/public/images/paymentMethods/unionpay.png deleted file mode 100644 index e9205908e16bbf2ac093972f6bfe0b10f027e37e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32719 zcmX_n1z1$y^ZwnXVd-v=B}5ve8ziJv5TqNWK|*qAkVZwi1(a@(S{gx0q*IV?>D>S2 z^L>7Q9zwZu&p9*m&b)Kx9HO)}mGN1pklYqGI3<00iClUyx6a2x!4yQonto|Mt1F&08;XH*3Jl%Zul=lfAp8 zxr;TAvzu-Dz9bC*FafHHaxc6yb{D+7jd~_z4iDzJUwzU&*nhGwypN&ooJ&b6ugZ)Q zltA<>K$R(U1rCQZGZSGmhn|FLn_zPj`P5Z$dl}ZxN5?q6X5A~lO7`72kKp1OY%a-c zs`2%Dxp6G{Zjti!!Gfj1qL|nEPih2_Joxtgx72t8dVpC`Q$az&QzubShEA#6T+~fS zV`PQyGUm0XEbp!_{l+U{%X#uW>7TOfQ{IRDCHTnojd^T=UU5hB$uD!Kh{7#LCof%E zORkg)8ER%YH3x(Vj^Q%w$j#0D$y1`j3bTTbYJhs2lWOA=rgpFBBMfh5ggoELohj2` zuy!-mvLL{S^owz2Ty05ppX{b4cx}coteEQiFJ~SAx;%j$A5`Duws;Bm!-ubH z#QEfZ;j;pvCtYF<4fXY!wKFjvMEfiNcm0D8Qn!Yxj?H3~uw{GEzO=l&YTa7PpoHm7 zg;>~YhWNg{-xsR$;m<{Fi}=hKfT9Uk;_=yz88@W&60lelibz=1fy#z|?1~`Vu#u>H z-Z7m@CraHqewnK8uN60eZ^nwXxSG+f`{KnJu_c#-hjyrb6bX+bplu4`($f$54$qddP;tX;=X`%Y|R8rI;-xeQSK-2H-FcpT) zxwEpe;_G24syxt!%Kp+*QyYAtAt*gWRgT_gRkF&Q8ii&ENLr51e7OAbZVas!B|6K9Y| zG5E2`w11!3 zMqo!_ff`sJO8FYy<;_PUXYhex^xJ@>lYBI9tQZ+RZS6BjOD-!=RYe8H%5FF5Y+C}@ zJP=+Y7qOL~-rY*0>mOCZ0aP1V?60SS zYbqcqj-WyX0KuV%SnU!yMRH3wm0&4BBuomlROr^GT`6{0Z93KAW561;pUV4L{9XpM z-FR{e3ZF89a2WkZfN1UJZv9<&NBa+iR!H0=E8N5pb|Glns}L4O-VDlENk0W514lNTUl1r%{NB&ugyo|vx31X3cY?uEL_*3D!dadExN;_tm52@@QNXt-(oz`2cp z&I?NA1ZoH4_7jZoG&MK>(N1OgePS%(1hhbuT6++R2k5%DVQvh>19O`9Au-dv=Pgz; zB<5PUMW|wyh7p|LB%yR3aN@=f$(WI_r+|tgxXy!fE{LrR8s8%5c4q!EsAU5sMRJ?9 z2NT>M93XD>_;EJI@Btd%__jVa^U==2W2EyV9v;-_w!X?Xm>c{$H;#X<(Gl`3!$E6= zs;n-1hYMbxN56l6W9H3p6P)<;s8nKn82&miCc8XjBcL4OAWe#9lcwYBx09+tv0 z&>RX>8}nPo@0={mWH1w)K}Cs9saz5SZ;EqrPF%&`MxH3+Ac-bsx0_Tfy#b{D{Z#Ot zH?g!!1H9$>HsE3(YeV_EYeht~CLiBm#$?-jaD^rmtOD0E1as%b(g^UWVr^aBr%j(j z9?UzBQE?e&1!GFlKn|*2skOB=dT<$~ayYW#s~Pr=i^&yky@8lH$a{-?4c)VzzwnWB zy?msnET0rlf`F}`i^OhV`ofS~pf9mit#_%@rTl8%NgS#e~9D)$EbUE9MgJ`3|M?~_lAAFB{dXI`|o(L-jE(! zl`9R>PlQFt@bjon_%7K`D90;BJ zmHgn?>2UEkH=m&?L1W(5fhP3M&JrYVe_w@Q8vSRF7;>oPIl1>-U}ta4R+Tta9sw)x zdSJs|I@ z9ocA-n~Hfgx~zp=MJF+5-se*>S?5Bw;B6*b;uv4I%n~TfP-h%@kP>Y@rOhZFc?JqsT zGCykar6Zdt>7OWDkY&@z#wAZ3i6-L-IT4hwX@`ll0IB~{SOfJ4$x{QG0$V3;wJJF8mor~)uwI0G+!Gu`*egR?TRU}M z0;K5u3<-+n$cQt(@}Jy~B&b zYJ#Ty=H3QL{8ZZ>#uCj11%D9}xmFK2wFSR|`tXY`V&dP066I0!!P9$X4lil8etrZg z35gCuH?4R25rF&O`~swmLrrIL_mo}2@@;x7&NfQ)3?>O`w9f={5 zFHP>k-^2JX*n$cKx6z2O+8eeg2+ndQT){$EOexaUNKf17%(MheRrw)dg?dVqFK zx@r3a%znk`Xq05+pIR+Fn6|<(>!@zZhO*aUQ20W7THw>t!R4Fzm6d;lWd!g{Z75OL z)STZ#v_Q-khsxN@TS&riK|H*{NqulpIp|E#?20u9msM84;67O~!<`t?b2Ha@9?n>R zVHEN=Yw5KzSZBIo$rl7rMEdCn|%k)CP@4{%;HvF5R)~S1lK>F zJb7|mN&x?)<2pxNRu(JR*kKB}_gK=t5?|=j=85;W`=?_t08uYIqNvb^1q}S>u}A@0 zvE3pHa`H>?IXV6%HKM*g&>MEEEBL9^qiq7KSm@^ zgE#@GbW~qZbL-+duRgspCw0>E2h!Oam@YRR+oJdH>0-xZn(FIc6;1>d1T%jIE)3I$ zS7TbS-yEr7Gb`w%cbxY0^n6M$CFp*mzzI3Ll&tAEVn9fai|lMcoDTrP9a&|pMh499 z--FDsC8>lMxvD-wkd&ii417dU2A3}6u0oVna96CO|aW3EYE_3uwr6d-{ zS?Hh2jVb%7rMA|dg9hR_jgasFXi z4X&Z16BXAg3MWC)0(c7fc(~XR*uP)$fZ`9Z2vm}JE!-t;_E4j?HtZd*n5mlC*Q22j zbW?w{T-!Ue#rceaeiZ@w(dwh?-6L-SUpVySp~lZ&zzdfgT;P*O!bcZ%XVMi3}>o_nqDr{J6{mKej8YrIN3RD}EAC9@c4&*-Brp1>0MVo-Zh)|D#T%FESqM(G*UBkDvDUXR-=E;WE6 zG$2^+Wd2w!^yTC$S}N6)Jzm6?a=`fq8xd;$ zA8Y6J^Y7a_+M8(?WNwJ@@2DL@T8Y$fH9^s0b1c57@ntOmx<_FYjpr#9`sD99l{GJ} zu@R&lOy8YNas8iXPM9a6&sdD~72#=yW)ti%bJ}gv-)wPU3kYqR)o(Q(PnOB1jR16# z-h5b?S-|q&Tq2)v=VHz3m`^&*~- z{H3=Qlg^f5l)MelRpoR8Qsme#6PbBoEc!{Ff&j>$4IQr106N1J8mRNnzKjdaMRl5+ zDDnK)hwSJG`k$`PUaE33- zRwYgo7I7p2z^z>2mR4{_PY5$OZtDbdBKoT>j$T2Y?E@{cu~EZQEo`x1EsgYh%D6#8 zXrLiR15MDK9jd`$ndo3LKT_qNqZBhaf!dId3HhSEg-@WPlk2JdVoqql;$-;f^O0Bd zB&s=|tG`t)>R8<`Ky5~C=kfmaGyy;*(0JaoLz}vRy9sqyLy!m{CC(IMn zpWz^FJcvRIGFCon0khza(bbF}$SJ|grM$CuRfH#kdDAR?!SR03Y1zJO5|UJwFQfoi zh7D1pUM~e=nFi9f9&UAdIKG@e^FHA9MS=aW{ z1apFV65;lxmWsNc;Zs)7ZW4`00A|Ls;tCUc9YwgoqWiNt3Y}s^Aqpod>iF@)PeARZ zq{wOA^`4e#^Wu9Kl_ViMv1!M_IuMMixMc*0gpa*{%hk)|e`xz}ZWaB2}h>`}TJ?8_O*{wmegvek{yGD3y8*W3oDj&d;;3b(a%ASz9 zjq5#=x3IWT$T?3?AFKm+yf$VcN>^D`@6@Whm{scsG$U$B!{%%6Km{<9ArF&)P0s+C zyx+Uq9)=(2@KxF3lf$Hh(<)eT*9^f9=tIr%=a!f`brbHyqSEABzLI3YZT_~^3J+9Y z*z*?d2P)Fz0}~%eNTRX4=Br36MIp->5|6`8pIo<9SyB>V!v*05ND%6i zJr2_ryi_+~q21BA08SLx_qzFus^biInf4U1sM}el>rHn2ffos1pi$=U6f#wNF;H_4 zomV2Af3(DR43a=Xi%=4*tmyWMt!js~lyMs{6XX4!eSMY9@evK_bPw!Wa{cc@)8~v7 zL%{R_WI5&(H**ptxXAem#m+f)CL?dLdee1>N*1IYv0@)9+9p>9r(Gw9n+8!$LNj1mCy7W@g;hGQr^ zRSjVF6^)h^GAyJuuAvse==4-crTc-b+ItC|MFx;GUxpne-#}r$fI{GUH{Qqd|L$%xQP*RzyF)qay^{ zgchf^e*+x(d-ZFb`Aww+Ayy<{4>oG3Fi`YRO;r^D`&t-gH}j1q^Ai}BBW>u)ilvHs zfl<#qfsa}wB!klPuF73VUN%b(FmU7fI{dE8>U`%h5Yj zOv8IzAnNIFRCdC&{$HppQxJ}<=+8C`wt6vkbqoY`8L6;C)p(+#Vy>Uh4LiIL!Va8$ zPC#E??g$ZE-o?y~7LBO-1Ol_ZS0{#VXo^Ia3SpE6HuuAcV$2+Co!Q`!eh;wMJ<#O0IO7L|1;NDm_E z={=IFsy0F2LP-$96&uf&Q^4n%J1A;ySokTy8$b~Zb2k!Z{vHkhorE6^*h?3fqV^ze zc$fsMn$d|IzVHeq5fHs!5(U9RmEfFGF)PaKhHh*JRzcaDtMn;1flT4v#Rfoz z9+sR6r{!GLiLXYLNv;rNS-csC#8E6J_V=C0xDJ+V9$6iV6<#kliGqaT$bv^A!dR$` zpLM+yPXPgHtgbigV81fg4}=?>jpgi0CWjBwKREcECfE>l75Wv;Oli(>@QL7H^?ky# z!$c%Z3rW2b1Yo;tU-5i>%ywr=IAKY_dOD{vIzI>^PpMd~`j3|5Bs>Yu9Ky-ruVfw(wI1ohF@x<&`2$4=CSPW3djE>7N zB?7f|k24|2->dYjK&mr77!I7^F-<>HcOeRrN(~qrKJXcm^|^oO(7;W}5KG{|jtdH? zI4JpH0jKOA!NS?mjPLPnd=>*$^Lg5L3{}UvdpK0*H(&wG@HAoduU|=9RX9MlP1C~E z=DA|K-2$_jv4}ff(u&^rU0_~PdOeDmgw`#B7=JM~iCUN5HCHZzyU(So}Vh-GbB~>;4CX9uvgQ(J>R#EaqprpZrv=^McJb z1-!1TnlfY@BCAM_54OKa46&6e_0~HZNdf%cFahV$>^4iv(Q(T&VR+aWDD>4RMY85& z0<3R_hRb-C)&2E6s#bIrL}5rFN|0d>VESLW;BVRrWEw_C9-LDOn&s}MpNutVLlb`Z zq~O!;VdzE33E+T-Xx(fE0Cu+oS`O9-n%R{VB`5IHfp#|xn;+!+_k6&+T8xAQJ1T@s zmb+8*?y0N)D_YpbKwAg5tV0cX*J*;M6Y~F&6kf?AB-Lp^F7r{9rvPoLFg^KCzsGKyh zyXVEf+%M|GidM3jM=(?>%Mb@>$2Gi<+Q`7n_FC)G#&IH?iVkD-|n5VL*MdOz-M>UWZ7N7VO zFhfgJ-$(~k8+j{JdwA$zH3T~^1%gY?e77qi_vD1*mYuuJB5&m zebRn~xV$9(;M!WoTu}>*uOW(hj}6~@K;!}Qx;#XqgO-E_n*5l8W;!`rnf~b&O=)b zd?bp#&^)H35gLE@fLS?p9+KeL&}F^SA^$j#31Er+e4(;!;Nk!^8~5H+1S%ruEh__= z*GUm(M7_ht#O-St!b2x}??hup<1z~mSTx3$Q$#yX9OVti8&up_Pr-U=hDqwc&Jw1@ z-2_u((|SRZguBihK7T59Ckg#Ww#an^P+r3myVWl(bssQ zh3a~4*`t&F9wb``O^~Zo^-(fID?UsHEO`H1k6XmNuidB2_FlGyj;LqCV#kD4NM;bH zD3gpK$2V_C#3d!(K#@(C+kpU|u<|~z`9jF$)$QNX-4UJaEF4*Ohv_#(Nsf65QGIcj zUEd~p%o9^Tk$eC9r*`zmBfG0RDXD{(Nntp6dqBmsULrGXG}nKOeO6S&G`COeZCxP%zV=rn$Ysr(>jzrU#QORg0J*;fbwYT;fGR! zumYl3{+|^wEjuwdadSGP0A{@`U->H4^;t+mgYr$5W%Z@$o5k^v(ZyEK+tFh5mGSiU zSJHYT7AYXHDSoBv9?RmYcI|n>p{E`H;i(%P!ObG$;rt?IzFk-6ppvq*47=;HnlTzEVYgjIXmFsgF4UA)`bF%48+-i67aGFXKKn_ z_DjdB{-lpqmauqZ7lvVa+dK1+g7&aa#6>7yS1w66gsIU3HXw~c`1XWQaS3$*V)4as zwzM8$2B!@_;$#6zh1ul;CaYqr2apuQPpLS-sKnlNyc(~3ouQk5s8tNc1pnuz_j7aU z_7<%J%--NYCYwR+a!Wv%r!q90pDv z4*uvE(Uck5=8Np1=+OjncDVNCk&!q+$h5dTyrqu(#^rhU!9!oSMG~Q)kC=VF!s_NWNUr+${i|5G@bU~Hckx?fAxwD=m%RrP?mx5rsU69*O7m0oR{WA z`#KkAhgC@-Kk@^w^D$*Z>C7$O`387g9iZZ;tV!8__0BQBNjODGkBe=%mJKnSKTue$ zc@pv--XiuCl9LxSzRhl{OAnOYx%baH4|bRjeX6{+mDo!WsD81Trew-aAf&wL`Sh1; zo1eT)5vP*%+wiZR8|Iq2tT?gkf*G$Q4`lf#=n|j%N|E-+ z@Y^J+j6qbF40sigkcbntk3#M9miOG?SUfT2-do^k-R?ktqDV4;iR`AhYf6;J$Ke-^ z2t+nLSlrJ_nVJ>~zbWM+Ya;m!Qx<6Wo{fEC?LR+xP#K|dDj^t19;yU<3xtT!_pNzP3Cf6rx=)*1C4R?A)iZM?F za*|XB^yYIF!aIOS7)H0fiOh{7a6mu2+Lr=+K77D?)4RGTsQFyn zZd? zd&7<$)N;!?5fk3mX(-ifJ-sBI9+Aes331YfJJIrQ*sM-RZilhH>7(@OBiS1XwiZ`Q z)(w`gn)*MC9eMit+wH%<8O?jO*P$>yHv8sE+Pq7f2w#;FFCvPv=}iW{coyRazy%i2 zVV3t8yh+yw$M|%NMSwiKzK1|rEqeX4s@ttZeW_^XNBu$$D$5Yr7l0^wstRDEtA{iP z-Jhlqp<&GXrU!CNudW~tUY1Zbq6e^+)0mS1K9}+MdLQGzr-6p=#2j$HnSrrG|6F}T z1^Cqfxa7AIK?INMLJ~|+MNs64wZ*Mz4@9(Ezd(U z%fkb(wsDmA6CdO?Mf;e*$XBCrb&^@?b}UEb&Oe|t*=@LHGt+4_^7yPWThjyFU~HG7 zj>~yM&qX-EB&o;lf@<%jvWX?l-rn9Mu&@vD@Mn(1VVgxk*S!F$49mSgO3%-UzU0|2 z2MNDBSD4dn4^*=;`I-_DcCe3S~|gN4=`>I3~LNz{ntNgpQs zXm3%fNw^Cp(rJA3FR5=ePPm8U#mbw_a1Sg6Vp4T=RCA{0_>f0^U2Pusj|I_)rlIrE zVrM5EA{o+n@hi;E&vU!Vo#n>|Aaf2B(#?oQ~A z;##o1aIN~Dm{^)wFIj6BCE@m%YLb6ltzB#SRu$Jx5OB+us43yhP2E*a+}#S4)u3Iv z0=L)U?=eU)5Xwicz@*ak{unh7lAZ=xrqATdQ@5Xs&@G`B<>twFULOON4;Y9N(rHvS zOwJSFxr`tnR|eRdfl48E+STZx$AaO`!@?24LS-EgtyBBQ0Wb?EDxPrQL0U-eWodGmnnYm&8B8?yvIabC5N}U}P zmHCR{3`w&Tx-V0&{mmuUNn}wph!kGBm?|+PG$u~qCiKs#oYuIdVGds%Rc}8Yz z)syw)0%4JNqnX`K88e;|(o#J)U8BboUh=FzES+u{f$3uXr8+cVBZxR=reG;JLVmR2 zAm&CxBg8Uzi#{CNamoDkj?r~-q8aY+Y5n&^V!#dqqd!dq%b>=#OWG4gm#-xMhztEn z{2MjqNmnekh!9hyIg5|<1yP;vTKv-?p{}R%DKfdZ>@nPZG5l*O0(oRTPL#Or#xB$G z1UEqmal=_eo^%d9vzf8_%{zS3r4ooMv@JuV?#giQ<#Dh1VkqZNYSSp4wM&kn5xM02 z^J5mON!Q{!G(*}~mG82#P=+;F2M*I%mS#a!m`qv$m}XA8U$(xUSWyBdg8rPi8cq<0 z?cL7zXpE2EM-~Wt9tx+nw!u@nN zZcTMrSb}6C3UfCjWDq+APY??PE(9zCscWy=Xv3!>k`Md{|Ab@rq@;!KKG%{PenN?? zbAgyeCmz10ahTAp)P2cp-{JRn$wi81+L!3%jW2nYv7WPMjf!G>+a1iPHV0FfyDi>h zVfo_B(s`x5uK70>X-BAePvcEDPUCKORLaZe4dpb^kXqd<0(qZX5LW+ z2HFc|c{dOYIY#YldUh4rtTd}pFiow=YPGwQe1|gswW608&!?qB9UL_r{u)>%eHY7b z)KZBlk|jcFo5gpP0=H!MmBLYKu1-ayGUBIJ;^e>sz2>uFz|JA`uO=N(HHFgE z9gg5KLA~@uuS9BMP6c1~Hyz$swiT{zDEqgrdn!EQM8|{!Gq%}LH#_aH>&=uJIey*v z@?JriMl#0VkCz90v-`}Qi%wcG#eFC3{5GmDJEt=F>Lvf+AXmP2x$cnTxpuT1x#C|P zr3ki>xc9zXu{9MXZ})Lx-S@v@L6=`|?mGSaVGS7~l7;aZ`TM5e4{IMQ@R>Uek?prn zrcTcqc-K=&A`AA~-f#Bh%rMCB8!uvITw~Q+L)WCU00B3lU2AvOLaVROiVlIa* zN9f;Z(XgCc^88WXT9cUfaz@P;QpC7YZ1=&i#LMkh%|8S=$x2aZKt`G{i4F;Zg}9p7 zW|3(f2SM$IUim&hri+mg%riZ=J{zT}7S3i=*n5)VOtTTX`6szp+I}PP8u)j&nUWsp zlQ(D4Dblm^I1aa&QuZ;wJ<`wF%CDC|t&bML0urnJhKnbo!s+mUcWMo}FvZnds9fF` z^&#S6>v;=~t~^-ymCkw8GCAo-(T*%Np^q%MHht}AZ+F!VyI9gH?_Oiq6q*3A`IaD+ z>~y__y?^N#Z$Hfl9)Xp&#?nUYf^WDLS`OC-t&XmL3%|}A_X?goo1)*}?0(K>TSmz- zy#@v53zg}Y_5frffAHXNpDO`N@AIof^o(o6uB6-?f#KnHp;T`57 zi&Up^w3htXFNxe#zwO9^@1lgf3;q}se%A+fC#l&u`l6~%;$CaQr@oDN?#35mwv?;u z(d#Kr0;Cmb+Yeg&4<29qLPPbC9(*&{P7^fQ?dn>lD~Zb}a=D7p@!qai)7CQ!*mAm$ znIflV^z18Jz$aohf-hMUSIl70c z%!YBC2%DYE;D(umjkf^;kC)B^Glgm0G6^nxd=6^}KU*4SaHkkH`)WJp`94rMs2YP9 zJ0zDCHd5KffASS=I>;w4#8rRX%hz6Qbaizn8h?jaYDNdxT61jZM6vgy{UmwaMVE_p zv;qD0p?7!8eOZOGa|MEy^Y?;TpMAq18hF1z{J5Ym;J z>|<}s^DMcSVHFbQm<;Wke$#MiTRlceGB4=p{Dd_p1OOts4~|7Be}3}8w%gzCwX~nY zGo4!c{mbI-BPw@57A;Scmm!tq@26^VF{PNT!0W~;orX_Q{U4#SiRef@U5qkq2iwBM z>CkyKUe$}`rH#(ZRoB^*L~pMTH*J5n@&^-lNKo_!O&X4nsUgatj@zkh>KY{X7!?{r z;~y>uiP2z@$-T!OykpcL%W+>UCShExeby8?Z9Aed)<+yljANQB3`%zcY{0!GyK^hqaog$3U1_?&R+T`ECWGRy6!x=1&hq3u z26h4Un%$H(>V3b?r9}gxmzmZ%IRMGm7cQkT^Bgye{toI2uJw4VVlx~=!yWV6Pq}EM zVXNwAY>|IK_fG%*8+DnMh;eywzV>;!`%l-WmW}yuoGqtK_J4$q$OOiIP&3^oi zx|n07d`ftS(1#R~;~+oVW+MEe;NQM9)5e!a&HBhV;r-PDBni;szbBh*WX#>P*=!Ak zE0to(ral9TbKXU4$|(svi6HoK$|*RUB}QXFkp7nZ`UYpR3?+yu={GKRF^f^7v849J zq5+R^(L4F%-@vZxr%tgP@<9f$0aoSR?b5>QEmZ%W6~UXqM{g=>{(b8jK0B<(yH978 zY^7(7VbXLZSp4((Qt=KRyLw;KYVS%GVY2zX`@H4xoupnTf?1V?s#{O%2BU?O1c;yr zBxXW$8K-L+2{rYvCE3HB72@u@`KnKjR`lmoUh7$% z6a42|uw~3;kgoTwsoYeDRsW!;#qh;bt|iW?i~`i%g?Csd=H!9!Qs=4iR91#tNMJVJ zM*MxcSG_gb(BPAiWgEmHCzr=TZgxUHKHPkiL?9m?RFdt>y#DrI%zL+%<4gNb+#-t4 zQa?vWj7TWY+`b`XAlbHF-csL(Auk`V^5-hH?7g%15(#)|-xgroC_&iu?<3n^u)W6p zclp=XaFdr2cE;u(@iQnYP!epKkqbiXnt95A1jr({Sw93y70VyYW$tws5U+gD+VK*} zldvm)^p|oZD@dY3`p0i6lD#5RQQd9vvD3guQ30dh^if)r4L1iKrEz1W#R9yP6(+af zjfcCa$`#A1ndWt#X+?n;YDQ=u-G*5REIfN1tgFiwPPqB& zQ5wZM0xs``hE>rw;7_oqaU3AEx_W2ACpzGEbnFn z!Yz%{q{b6crr5S-^(1lr_bSMju3mRLBfBRk9Ls^D{;9v&cA{8|b1jS`fwlCElk~_% zC7D;VzA^>>+^e;p8;91|#`b#H4k5e${KMkN6!wSrzN9qYkgFK=_(p=5JY$eNa-v^c zsAaVGWa#ZkCP42Jr}M;T8T{wEj+5^?*yFBoio-GVgTkkE<`%WJGWAGrW$f`zUVy13 z4AppIF+OpmccD`G$yLA}uxhxwM1$No)m_#F%I;mEi0k9xx_W@PNAM~%rK%GG#84G3 zFyRnLydY7LX8A2RB<6cFF^On@zu1qP@%DgCqS*ix)%T@$SdvuY``6)9SY12!_IN9O z&iL_W#Y9vqh56K^dIu#ijzTn{gT*OwT&D3wqpD{1%a;ee=XW>4+hf z9N5VySCHNnuVo1I@ka|duXA@@eHW}z61PbZHT1U_M1lucSRoGDFExKo&3g=3WIVQc z`)ozW@zXL)R)7?kDGNNW(8tI&BMh@6;X?z4_nt&10CvEK>so9~^WFE0{R+BxW`ni> z*{&7#q$319ONt!!2u zdbm206@$~&ojriYOg|-a!8zQ1y;U57lKtLswE1YqWu5RYwj8+}NSJ68kT-qMnQ^p& zfw*mLHD_d7xS(S2w>CD4%5-w^yG@)bu$TSbd4w_&>it*?Xj*Dv{g$$6xI? zv`XboJa6#jeyXEnZsnwgn|EFdU3N>mc!aFJgw~MV(#N