├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .gitlab-ci.yml ├── .gitlab ├── agents │ └── k8s-cluster-1 │ │ └── config.yaml └── merge_request_templates │ └── General.md ├── .gitpod.yml ├── .whitesource ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── assets ├── database-lab-dark-mode.png ├── database-lab-dark-mode.svg ├── database-lab-light-mode.png ├── database-lab-light-mode.svg ├── db-lab.png ├── dle-demo-animated.gif ├── dle-logo-only-transparent-590x373.png ├── dle-logo-only-white-bg-640x640.png ├── dle-simple.png ├── dle-simple.svg ├── dle-square-1024x1024.png ├── dle-square-220x220.png ├── dle-square-512x512.png ├── dle.svg ├── dle_button.svg └── star.gif ├── engine ├── .dockerignore ├── .gitlab-ci.yml ├── .golangci.yml ├── Dockerfile.ci-checker ├── Dockerfile.dblab-cli ├── Dockerfile.dblab-server ├── Dockerfile.dblab-server-debug ├── Dockerfile.dblab-server-zfs08 ├── Dockerfile.swagger-ui ├── Makefile ├── api │ ├── postman │ │ ├── dblab.postman_collection.json │ │ └── dblab.postman_environment.json │ ├── swagger-spec │ │ └── dblab_server_swagger.yaml │ └── swagger-ui │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── index.css │ │ ├── index.html │ │ ├── oauth2-redirect.html │ │ ├── swagger-initializer.js │ │ ├── swagger-ui-bundle.js │ │ ├── swagger-ui-bundle.js.map │ │ ├── swagger-ui-es-bundle-core.js │ │ ├── swagger-ui-es-bundle-core.js.map │ │ ├── swagger-ui-es-bundle.js │ │ ├── swagger-ui-es-bundle.js.map │ │ ├── swagger-ui-standalone-preset.js │ │ ├── swagger-ui-standalone-preset.js.map │ │ ├── swagger-ui.css │ │ ├── swagger-ui.css.map │ │ ├── swagger-ui.js │ │ └── swagger-ui.js.map ├── cmd │ ├── cli │ │ ├── commands │ │ │ ├── client.go │ │ │ ├── clone │ │ │ │ ├── actions.go │ │ │ │ └── command_list.go │ │ │ ├── config │ │ │ │ ├── actions.go │ │ │ │ ├── command_list.go │ │ │ │ ├── duration.go │ │ │ │ ├── environment.go │ │ │ │ └── file.go │ │ │ ├── errors.go │ │ │ ├── global │ │ │ │ ├── actions.go │ │ │ │ └── command_list.go │ │ │ ├── instance │ │ │ │ ├── actions.go │ │ │ │ └── command_list.go │ │ │ ├── port_forwarding.go │ │ │ └── snapshot │ │ │ │ ├── actions.go │ │ │ │ └── command_list.go │ │ ├── main.go │ │ └── templates │ │ │ └── help.go │ ├── database-lab │ │ └── main.go │ └── runci │ │ └── main.go ├── configs │ ├── config.example.ci_checker.yml │ ├── config.example.logical_generic.yml │ ├── config.example.logical_rds_iam.yml │ ├── config.example.physical_generic.yml │ ├── config.example.physical_pgbackrest.yml │ ├── config.example.physical_walg.yml │ └── standard │ │ └── postgres │ │ ├── control │ │ ├── pg_hba.conf │ │ └── postgresql.conf │ │ └── default │ │ ├── 10 │ │ ├── pg_hba.conf │ │ └── postgresql.dblab.postgresql.conf │ │ ├── 11 │ │ ├── pg_hba.conf │ │ └── postgresql.dblab.postgresql.conf │ │ ├── 12 │ │ ├── pg_hba.conf │ │ └── postgresql.dblab.postgresql.conf │ │ ├── 13 │ │ ├── pg_hba.conf │ │ └── postgresql.dblab.postgresql.conf │ │ ├── 14 │ │ ├── pg_hba.conf │ │ └── postgresql.dblab.postgresql.conf │ │ ├── 15 │ │ ├── pg_hba.conf │ │ └── postgresql.dblab.postgresql.conf │ │ ├── 16 │ │ ├── pg_hba.conf │ │ └── postgresql.dblab.postgresql.conf │ │ ├── 17 │ │ ├── pg_hba.conf │ │ └── postgresql.dblab.postgresql.conf │ │ └── 9.6 │ │ ├── pg_hba.conf │ │ └── postgresql.dblab.postgresql.conf ├── deploy │ └── swagger-ui.yaml ├── go.mod ├── go.sum ├── internal │ ├── billing │ │ └── billing.go │ ├── cloning │ │ ├── base.go │ │ ├── base_test.go │ │ ├── snapshots.go │ │ ├── snapshots_test.go │ │ ├── storage.go │ │ ├── storage_test.go │ │ └── wrapper.go │ ├── diagnostic │ │ ├── logs.go │ │ ├── logs_integration_test.go │ │ └── logs_test.go │ ├── embeddedui │ │ ├── embedded_ui.go │ │ ├── embedded_ui_integration_test.go │ │ └── embedded_ui_test.go │ ├── observer │ │ ├── artifacts.go │ │ ├── observer.go │ │ ├── observer_test.go │ │ ├── observing_clone.go │ │ ├── queries.go │ │ ├── session.go │ │ ├── session_test.go │ │ ├── sql.go │ │ └── stats.go │ ├── platform │ │ ├── platform.go │ │ └── platform_test.go │ ├── portfwd │ │ └── sshtunnel.go │ ├── provision │ │ ├── databases │ │ │ └── postgres │ │ │ │ ├── pgconfig │ │ │ │ ├── configuration.go │ │ │ │ └── configuration_test.go │ │ │ │ ├── postgres.go │ │ │ │ ├── postgres_mgmt.go │ │ │ │ ├── postgres_mgmt_test.go │ │ │ │ └── postgres_test.go │ │ ├── docker │ │ │ ├── docker.go │ │ │ └── docker_test.go │ │ ├── mode_local.go │ │ ├── mode_local_test.go │ │ ├── pool │ │ │ ├── block_devices.go │ │ │ ├── fstype_bsd.go │ │ │ ├── fstype_linux.go │ │ │ ├── fstype_windows.go │ │ │ ├── manager.go │ │ │ └── pool_manager.go │ │ ├── port_checker.go │ │ ├── provision.go │ │ ├── resources │ │ │ ├── appconfig.go │ │ │ ├── pool.go │ │ │ └── resources.go │ │ ├── runners │ │ │ └── runners.go │ │ └── thinclones │ │ │ ├── lvm │ │ │ ├── lvm.go │ │ │ └── lvmanager.go │ │ │ ├── manager.go │ │ │ └── zfs │ │ │ ├── snapshots_filter.go │ │ │ ├── snapshots_filter_test.go │ │ │ ├── zfs.go │ │ │ └── zfs_test.go │ ├── retrieval │ │ ├── components │ │ │ └── components.go │ │ ├── config │ │ │ └── config.go │ │ ├── dbmarker │ │ │ └── dbmarker.go │ │ ├── engine │ │ │ ├── engine.go │ │ │ └── postgres │ │ │ │ ├── job_builder.go │ │ │ │ ├── logical │ │ │ │ ├── activity.go │ │ │ │ ├── activity_test.go │ │ │ │ ├── archive_types.go │ │ │ │ ├── archive_types_test.go │ │ │ │ ├── dump.go │ │ │ │ ├── dump_default.go │ │ │ │ ├── dump_integration_test.go │ │ │ │ ├── dump_rds.go │ │ │ │ ├── logical.go │ │ │ │ ├── logical_test.go │ │ │ │ ├── restore.go │ │ │ │ └── restore_test.go │ │ │ │ ├── physical │ │ │ │ ├── custom.go │ │ │ │ ├── custom_test.go │ │ │ │ ├── pgbackrest.go │ │ │ │ ├── pgbackrest_test.go │ │ │ │ ├── physical.go │ │ │ │ ├── physical_test.go │ │ │ │ ├── wal_g.go │ │ │ │ └── wal_g_test.go │ │ │ │ ├── snapshot │ │ │ │ ├── errors.go │ │ │ │ ├── logical.go │ │ │ │ ├── physical.go │ │ │ │ ├── physical_integration_test.go │ │ │ │ ├── physical_test.go │ │ │ │ └── snapshot.go │ │ │ │ └── tools │ │ │ │ ├── activity │ │ │ │ └── activity.go │ │ │ │ ├── cont │ │ │ │ ├── container.go │ │ │ │ └── container_test.go │ │ │ │ ├── db │ │ │ │ ├── image_content.go │ │ │ │ └── pg.go │ │ │ │ ├── defaults │ │ │ │ └── defaults.go │ │ │ │ ├── dump.go │ │ │ │ ├── fs │ │ │ │ └── tools.go │ │ │ │ ├── health │ │ │ │ └── healthcheck.go │ │ │ │ ├── pgtool │ │ │ │ └── pgtool.go │ │ │ │ ├── query │ │ │ │ └── preprocessor.go │ │ │ │ ├── tools.go │ │ │ │ └── tools_test.go │ │ ├── options │ │ │ └── options.go │ │ ├── refresh_errors.go │ │ ├── retrieval.go │ │ ├── retrieval_test.go │ │ ├── state.go │ │ ├── state_test.go │ │ ├── status │ │ │ └── retrieval_status.go │ │ ├── validator.go │ │ └── validator_test.go │ ├── runci │ │ ├── clone.go │ │ ├── config.go │ │ ├── handlers.go │ │ ├── server.go │ │ └── source │ │ │ ├── github.go │ │ │ └── source.go │ ├── srv │ │ ├── api │ │ │ ├── errors.go │ │ │ ├── errors_test.go │ │ │ └── util.go │ │ ├── billing.go │ │ ├── config.go │ │ ├── config │ │ │ └── config.go │ │ ├── config_test.go │ │ ├── mw │ │ │ ├── auth.go │ │ │ ├── auth_test.go │ │ │ └── logging.go │ │ ├── routes.go │ │ ├── server.go │ │ ├── ws.go │ │ ├── ws │ │ │ ├── ping.go │ │ │ ├── token.go │ │ │ └── token_test.go │ │ └── ws_test.go │ ├── telemetry │ │ ├── events.go │ │ └── telemetry.go │ └── validator │ │ ├── validator.go │ │ └── validator_test.go ├── pkg │ ├── client │ │ ├── dblabapi │ │ │ ├── client.go │ │ │ ├── client_test.go │ │ │ ├── clone.go │ │ │ ├── clone_test.go │ │ │ ├── snapshot.go │ │ │ ├── snapshot_test.go │ │ │ ├── status.go │ │ │ ├── status_test.go │ │ │ └── types │ │ │ │ ├── clone.go │ │ │ │ └── observation.go │ │ └── platform │ │ │ ├── client.go │ │ │ ├── client_test.go │ │ │ ├── observation.go │ │ │ ├── telemetry.go │ │ │ └── token.go │ ├── config │ │ ├── config.go │ │ ├── config_test.go │ │ ├── global │ │ │ └── config.go │ │ └── loaders.go │ ├── log │ │ ├── filtering.go │ │ └── log.go │ ├── models │ │ ├── admin.go │ │ ├── clone.go │ │ ├── configuration.go │ │ ├── database.go │ │ ├── error.go │ │ ├── fs.go │ │ ├── instance_status.go │ │ ├── local_time.go │ │ ├── local_time_test.go │ │ ├── observation.go │ │ ├── retrieval.go │ │ ├── retrieval_test.go │ │ ├── snapshot.go │ │ ├── status.go │ │ └── sync.go │ └── util │ │ ├── backup │ │ ├── backup.go │ │ ├── backup_test.go │ │ ├── collection.go │ │ └── utils.go │ │ ├── bytes.go │ │ ├── clones.go │ │ ├── config.go │ │ ├── config_test.go │ │ ├── engine │ │ └── engine.go │ │ ├── networks │ │ ├── networks.go │ │ └── networks_test.go │ │ ├── pglog │ │ ├── activity.go │ │ └── activity_test.go │ │ ├── projection │ │ ├── common_test.go │ │ ├── helpers.go │ │ ├── json.go │ │ ├── load_json_test.go │ │ ├── load_yaml_test.go │ │ ├── multi_group_test.go │ │ ├── operations.go │ │ ├── store_json_test.go │ │ ├── store_yaml_test.go │ │ ├── tags.go │ │ ├── types.go │ │ └── yaml.go │ │ ├── ptypes │ │ └── mapping.go │ │ ├── slices.go │ │ ├── slices_test.go │ │ ├── testing.go │ │ ├── time.go │ │ ├── time_test.go │ │ └── yaml │ │ ├── custom.go │ │ ├── custom_test.go │ │ ├── default.go │ │ ├── mask.go │ │ ├── mask_test.go │ │ └── path.go ├── scripts │ ├── adjust_configs.sh │ ├── bash_autocomplete │ ├── ci_docker_build_push.sh │ ├── cleanup_zfs_snapshot.sh │ ├── cli_install.sh │ ├── create_zfs_snapshot.sh │ ├── do.sh │ └── preprocess_example.sh ├── test │ ├── 1.synthetic.sh │ ├── 2.logical_generic.sh │ ├── 3.physical_walg.sh │ ├── 4.physical_basebackup.sh │ ├── 5.logical_rds.sh │ ├── _cleanup.sh │ ├── _prerequisites.ubuntu.sh │ └── _zfs.file.sh ├── testdata │ └── testdata.go └── version │ ├── version.go │ └── version_test.go ├── translations ├── README.german.md ├── README.portuguese-br.md ├── README.russian.md ├── README.spanish.md ├── README.ukrainian.md └── unfinished-review-needed │ └── .gitkeep └── ui ├── .dockerignore ├── .gitignore ├── .gitlab-ci.yml ├── .npmrc ├── .prettierrc ├── .stylelintrc ├── README.md ├── cspell.json ├── package.json ├── packages ├── ce │ ├── .dockerignore │ ├── .env │ ├── .eslintrc │ ├── .gitlab-ci.yml │ ├── .npmrc │ ├── Dockerfile │ ├── ci_docker_build_push.sh │ ├── craco.config.js │ ├── cypress.config.ts │ ├── cypress │ │ └── e2e │ │ │ └── tabs.cy.js │ ├── docker-entrypoint.sh │ ├── nginx.conf │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── App │ │ │ ├── Auth │ │ │ │ ├── Card │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.module.scss │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.scss │ │ │ ├── Instance │ │ │ │ ├── Clones │ │ │ │ │ ├── Clone │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── CreateClone │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── Page │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Layout │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.scss │ │ │ ├── Menu │ │ │ │ ├── Header │ │ │ │ │ ├── icons │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.module.scss │ │ │ │ ├── Instances │ │ │ │ │ ├── icons │ │ │ │ │ │ └── plus.svg │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.module.scss │ │ │ │ ├── SignOutModal │ │ │ │ │ └── index.tsx │ │ │ │ ├── StickyTopBar │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── styles.module.scss │ │ │ │ │ └── utils │ │ │ │ │ │ └── index.ts │ │ │ │ ├── icons │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.scss │ │ │ └── index.tsx │ │ ├── api │ │ │ ├── clones │ │ │ │ ├── createClone.ts │ │ │ │ ├── destroyClone.ts │ │ │ │ ├── getClone.ts │ │ │ │ ├── resetClone.ts │ │ │ │ └── updateClone.ts │ │ │ ├── configs │ │ │ │ ├── activateBilling.ts │ │ │ │ ├── getBillingStatus.ts │ │ │ │ ├── getConfig.ts │ │ │ │ ├── getFullConfig.ts │ │ │ │ ├── getSeImages.ts │ │ │ │ ├── testDbSource.ts │ │ │ │ └── updateConfig.ts │ │ │ ├── engine │ │ │ │ ├── getEngine.ts │ │ │ │ ├── getWSToken.ts │ │ │ │ └── initWS.ts │ │ │ ├── instances │ │ │ │ ├── getInstance.ts │ │ │ │ └── getInstanceRetrieval.ts │ │ │ └── snapshots │ │ │ │ └── getSnapshots.ts │ │ ├── components │ │ │ ├── NavPath │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.scss │ │ │ └── PageContainer │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.scss │ │ ├── config │ │ │ ├── env.ts │ │ │ └── routes.tsx │ │ ├── helpers │ │ │ ├── edition.ts │ │ │ ├── localStorage.ts │ │ │ └── request.ts │ │ ├── index.scss │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ └── stores │ │ │ └── app.ts │ └── tsconfig.json └── shared │ ├── .gitlab-ci.yml │ ├── .npmrc │ ├── components │ ├── AlertSnackbar │ │ ├── index.tsx │ │ └── useAlertSnackbar.tsx │ ├── Button │ │ └── index.tsx │ ├── Button2 │ │ ├── index.tsx │ │ └── styles.module.scss │ ├── DestroyCloneModal │ │ └── index.tsx │ ├── DestroyCloneRestrictionModal │ │ └── index.tsx │ ├── ErrorStub │ │ └── index.tsx │ ├── FormattedText │ │ ├── index.tsx │ │ └── styles.module.scss │ ├── GatewayLink │ │ └── index.tsx │ ├── HorizontalScrollContainer │ │ ├── index.tsx │ │ ├── types.ts │ │ └── utils.ts │ ├── ImportantText │ │ └── index.tsx │ ├── Link2 │ │ ├── index.tsx │ │ └── styles.module.scss │ ├── MenuButton │ │ ├── index.tsx │ │ └── styles.module.scss │ ├── Modal │ │ └── index.tsx │ ├── PageSpinner │ │ ├── index.tsx │ │ └── styles.module.scss │ ├── ResetCloneModal │ │ └── index.tsx │ ├── SectionTitle │ │ └── index.tsx │ ├── Select │ │ └── index.tsx │ ├── SimpleModalControls │ │ └── index.tsx │ ├── Spinner │ │ ├── icon.tsx │ │ ├── index.tsx │ │ └── styles.module.scss │ ├── Status │ │ ├── index.tsx │ │ └── styles.module.scss │ ├── StubContainer │ │ └── index.tsx │ ├── StubSpinner │ │ └── index.tsx │ ├── StubSpinnerFlex │ │ ├── index.tsx │ │ └── styles.module.scss │ ├── SyntaxHighlight │ │ └── index.tsx │ ├── Table │ │ ├── RowMenu │ │ │ └── index.tsx │ │ └── index.tsx │ ├── Text │ │ └── index.tsx │ ├── TextField │ │ └── index.tsx │ └── Tooltip │ │ └── index.tsx │ ├── config │ ├── index.ts │ └── links.ts │ ├── craco.config.js │ ├── helpers │ ├── getEntropy.ts │ ├── localStorage.ts │ └── request.ts │ ├── hooks │ └── useWindowDimensions.ts │ ├── icons │ ├── ArrowDropDown │ │ └── index.tsx │ ├── Circle │ │ └── index.tsx │ ├── External │ │ └── index.tsx │ ├── Info │ │ └── index.tsx │ ├── Renewable │ │ └── index.tsx │ ├── Shield │ │ └── index.tsx │ └── Warning │ │ └── index.tsx │ ├── meta.json │ ├── package.json │ ├── pages │ ├── Clone │ │ ├── Status │ │ │ └── index.tsx │ │ ├── context.ts │ │ ├── index.tsx │ │ ├── stores │ │ │ └── Main.ts │ │ └── useCreatedStores.ts │ ├── Configuration │ │ ├── Header │ │ │ └── index.tsx │ │ ├── InputWithTooltip │ │ │ └── index.tsx │ │ ├── ResponseMessage │ │ │ └── index.tsx │ │ ├── configOptions.ts │ │ ├── index.tsx │ │ ├── styles.module.scss │ │ ├── tooltipText.tsx │ │ ├── useForm.ts │ │ └── utils │ │ │ └── index.ts │ ├── CreateClone │ │ ├── index.tsx │ │ ├── stores │ │ │ └── Main.ts │ │ ├── styles.module.scss │ │ ├── useCreatedStores.ts │ │ └── useForm.ts │ ├── Instance │ │ ├── Clones │ │ │ ├── Header │ │ │ │ ├── Item │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.module.scss │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.scss │ │ │ └── index.tsx │ │ ├── ClonesModal │ │ │ ├── index.tsx │ │ │ └── utils.ts │ │ ├── InactiveInstance │ │ │ ├── index.tsx │ │ │ └── utils.ts │ │ ├── Info │ │ │ ├── Connection │ │ │ │ ├── ConnectModal │ │ │ │ │ ├── Content │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── utils.ts │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Details │ │ │ │ └── index.tsx │ │ │ ├── Disks │ │ │ │ ├── Disk │ │ │ │ │ ├── ActionsMenu │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Marker │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── ProgressBar │ │ │ │ │ │ ├── PointerIcon.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── Status │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── Icons │ │ │ │ └── index.tsx │ │ │ ├── Retrieval │ │ │ │ ├── RefreshFailedAlert │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.module.scss │ │ │ │ ├── RetrievalModal │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.module.scss │ │ │ │ ├── RetrievalTable │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.module.scss │ │ │ │ ├── index.tsx │ │ │ │ └── utils.ts │ │ │ ├── Snapshots │ │ │ │ ├── Calendar │ │ │ │ │ ├── Day │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── TimeLine │ │ │ │ │ ├── Day │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── utils.ts │ │ │ ├── Status │ │ │ │ ├── InstanceResponseModal │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.module.scss │ │ │ │ ├── index.tsx │ │ │ │ ├── styles.module.scss │ │ │ │ └── utils.ts │ │ │ ├── components │ │ │ │ ├── Property │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.module.scss │ │ │ │ ├── Section │ │ │ │ │ └── index.tsx │ │ │ │ └── ValueStatus │ │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── SnapshotsModal │ │ │ ├── index.tsx │ │ │ └── utils.ts │ │ ├── Tabs │ │ │ └── index.tsx │ │ ├── components │ │ │ ├── ClonesList │ │ │ │ ├── ConnectionModal │ │ │ │ │ └── index.tsx │ │ │ │ ├── MenuCell │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.scss │ │ │ ├── ErrorStub │ │ │ │ └── index.tsx │ │ │ ├── ModalReloadButton │ │ │ │ └── index.tsx │ │ │ └── Tags │ │ │ │ ├── Tag │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ ├── context.ts │ │ ├── index.tsx │ │ ├── stores │ │ │ ├── ClonesModal.ts │ │ │ ├── Main.ts │ │ │ └── SnapshotsModal.ts │ │ ├── styles.scss │ │ └── useCreatedStores.ts │ └── Logs │ │ ├── Icons │ │ └── PlusIcon.tsx │ │ ├── constants │ │ └── index.ts │ │ ├── hooks │ │ └── useWsScroll.tsx │ │ ├── index.tsx │ │ ├── utils │ │ └── index.ts │ │ ├── wsLogs.ts │ │ └── wsSnackbar.ts │ ├── react-app-env.d.ts │ ├── scripts │ ├── copy-assets.js │ └── pack.js │ ├── stores │ └── Snapshots.ts │ ├── styles │ ├── colors.ts │ ├── global.scss │ ├── icons.tsx │ ├── mixins.scss │ ├── styles.ts │ ├── theme.ts │ ├── vars.scss │ └── vars.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── types │ ├── api │ │ ├── endpoints │ │ │ ├── createClone.ts │ │ │ ├── destroyClone.ts │ │ │ ├── getClone.ts │ │ │ ├── getConfig.ts │ │ │ ├── getEngine.ts │ │ │ ├── getFullConfig.ts │ │ │ ├── getInstance.ts │ │ │ ├── getInstanceRetrieval.ts │ │ │ ├── getSeImages.ts │ │ │ ├── getSnapshots.ts │ │ │ ├── getWSToken.ts │ │ │ ├── initWS.ts │ │ │ ├── refreshInstance.ts │ │ │ ├── resetClone.ts │ │ │ ├── testDbSource.ts │ │ │ ├── updateClone.ts │ │ │ └── updateConfig.ts │ │ └── entities │ │ │ ├── clone.ts │ │ │ ├── config.ts │ │ │ ├── dbSource.ts │ │ │ ├── instance.ts │ │ │ ├── instanceRetrieval.ts │ │ │ ├── instanceState.ts │ │ │ ├── pool.ts │ │ │ ├── snapshot.ts │ │ │ └── wsToken.ts │ └── byte-size │ │ └── index.d.ts │ └── utils │ ├── api.ts │ ├── clone.ts │ ├── connection.ts │ ├── date.ts │ ├── instance.ts │ ├── numbers.ts │ ├── react.ts │ ├── snapshot.ts │ ├── strings.ts │ └── units.ts ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-detectable=false 2 | *.ts linguist-detectable=false 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: postgres-ai 4 | patreon: Database_Lab # Replace with a single Patreon username 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | 4 | engine/bin/ 5 | /db-lab-run/ 6 | 7 | # Deploy contains Kubernetes configs with secrets, remove from .gitignore when generalized. 8 | /deploy/ 9 | 10 | /engine/configs/config.yml 11 | /engine/configs/server.yml 12 | /engine/configs/run_ci.yaml 13 | /engine/configs/ci_checker.yml 14 | 15 | engine/meta 16 | 17 | ui/packages/shared/dist/ 18 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | DOCKER_DRIVER: overlay2 3 | 4 | workflow: 5 | rules: 6 | - if: $CI_PIPELINE_SOURCE == "merge_request_event" 7 | - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH 8 | - if: $CI_COMMIT_TAG 9 | - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH 10 | when: never 11 | 12 | include: 13 | - local: 'engine/.gitlab-ci.yml' 14 | - local: 'ui/.gitlab-ci.yml' 15 | 16 | empty-job: 17 | stage: test 18 | script: 19 | - echo 'success' 20 | rules: 21 | - if: $CI_PIPELINE_SOURCE == "merge_request_event" 22 | when: always 23 | - if: $CI_PIPELINE_SOURCE == "merge_request_event" 24 | changes: 25 | - engine/**/* 26 | - ui/**/* 27 | when: never 28 | -------------------------------------------------------------------------------- /.gitlab/agents/k8s-cluster-1/config.yaml: -------------------------------------------------------------------------------- 1 | ci_access: 2 | projects: 3 | - id: postgres-ai/database-lab 4 | -------------------------------------------------------------------------------- /.gitlab/merge_request_templates/General.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | [give description of a MR] 4 | 5 | # Related issue 6 | 7 | [give full link to the related issue] 8 | 9 | # Examples 10 | 11 | [present examples for reviewers: new CLI output, screenshots, etc] 12 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - before: > 3 | [[ ! -z $GNUGPG ]] && 4 | cd ~ && 5 | rm -rf .gnupg && 6 | echo $GNUGPG | base64 -d | tar --no-same-owner -xzvf - 7 | 8 | # test -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff" 8 | }, 9 | "issueSettings": { 10 | "minSeverityLevel": "LOW" 11 | } 12 | } -------------------------------------------------------------------------------- /assets/database-lab-dark-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/assets/database-lab-dark-mode.png -------------------------------------------------------------------------------- /assets/database-lab-light-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/assets/database-lab-light-mode.png -------------------------------------------------------------------------------- /assets/db-lab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/assets/db-lab.png -------------------------------------------------------------------------------- /assets/dle-demo-animated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/assets/dle-demo-animated.gif -------------------------------------------------------------------------------- /assets/dle-logo-only-transparent-590x373.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/assets/dle-logo-only-transparent-590x373.png -------------------------------------------------------------------------------- /assets/dle-logo-only-white-bg-640x640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/assets/dle-logo-only-white-bg-640x640.png -------------------------------------------------------------------------------- /assets/dle-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/assets/dle-simple.png -------------------------------------------------------------------------------- /assets/dle-square-1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/assets/dle-square-1024x1024.png -------------------------------------------------------------------------------- /assets/dle-square-220x220.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/assets/dle-square-220x220.png -------------------------------------------------------------------------------- /assets/dle-square-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/assets/dle-square-512x512.png -------------------------------------------------------------------------------- /assets/star.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/assets/star.gif -------------------------------------------------------------------------------- /engine/.dockerignore: -------------------------------------------------------------------------------- 1 | meta/ -------------------------------------------------------------------------------- /engine/Dockerfile.ci-checker: -------------------------------------------------------------------------------- 1 | FROM docker:20.10.24 2 | 3 | # Install dependencies. 4 | RUN apk update && apk add --no-cache bash 5 | 6 | WORKDIR /home/dblab 7 | 8 | COPY ./bin/run-ci ./bin/run-ci 9 | 10 | CMD ./bin/run-ci 11 | -------------------------------------------------------------------------------- /engine/Dockerfile.dblab-cli: -------------------------------------------------------------------------------- 1 | FROM docker:20.10.24 2 | 3 | # Install dependencies. 4 | RUN apk update && apk add --no-cache bash jq tzdata 5 | 6 | WORKDIR /home/dblab 7 | COPY ./bin/dblab ./bin/dblab 8 | CMD ./bin/dblab 9 | -------------------------------------------------------------------------------- /engine/Dockerfile.dblab-server: -------------------------------------------------------------------------------- 1 | # See Guides to learn how to start a container: https://postgres.ai/docs/how-to-guides/administration/engine-manage 2 | 3 | FROM docker:20.10.24 4 | 5 | # Install dependencies 6 | RUN apk update \ 7 | && apk add --no-cache zfs lvm2 bash util-linux tzdata 8 | 9 | WORKDIR /home/dblab 10 | 11 | COPY ./bin/dblab-server ./bin/dblab-server 12 | COPY ./configs/standard ./standard 13 | COPY ./api ./api 14 | COPY ./scripts ./scripts 15 | 16 | CMD ./bin/dblab-server 17 | -------------------------------------------------------------------------------- /engine/Dockerfile.dblab-server-debug: -------------------------------------------------------------------------------- 1 | # How to start a container: https://postgres.ai/docs/how-to-guides/administration/engine-manage 2 | 3 | # Compile stage 4 | FROM golang:1.23 AS build-env 5 | 6 | # Build Delve 7 | RUN go install github.com/go-delve/delve/cmd/dlv@latest 8 | 9 | # Build DLE (Uncomment if the binary doesn't compile locally). 10 | # ADD . /dockerdev 11 | # WORKDIR /dockerdev 12 | # RUN GO111MODULE=on CGO_ENABLED=0 go build -gcflags="all=-N -l" -o /dblab-server-debug ./cmd/database-lab/main.go 13 | 14 | # Final stage 15 | FROM docker:20.10.12 16 | 17 | # Install dependencies 18 | RUN apk update \ 19 | && apk add zfs=2.1.4-r0 --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main \ 20 | && apk add --no-cache lvm2 bash util-linux tzdata 21 | 22 | WORKDIR /home/dblab 23 | 24 | EXPOSE 2345 40000 25 | 26 | # Replace if the binary doesn't compile locally. 27 | # COPY --from=build-env /dblab-server-debug ./bin/dblab-server-debug 28 | COPY ./bin/dblab-server-debug ./bin/dblab-server-debug 29 | 30 | COPY --from=build-env /go/bin/dlv ./ 31 | COPY ./configs/standard ./standard 32 | COPY ./api ./api 33 | COPY ./scripts ./scripts 34 | 35 | CMD ["./dlv", "--listen=:40000", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "./bin/dblab-server-debug"] 36 | -------------------------------------------------------------------------------- /engine/Dockerfile.dblab-server-zfs08: -------------------------------------------------------------------------------- 1 | # See Guides to learn how to start a container: https://postgres.ai/docs/how-to-guides/administration/engine-manage 2 | 3 | FROM docker:20.10.24 4 | 5 | # Install dependencies. 6 | RUN apk update && apk add zfs=0.8.4-r0 --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/v3.12/main \ 7 | && apk add --no-cache lvm2 bash util-linux tzdata 8 | 9 | WORKDIR /home/dblab 10 | 11 | COPY ./bin/dblab-server ./bin/dblab-server 12 | COPY ./configs/standard ./standard 13 | COPY ./api ./api 14 | COPY ./scripts ./scripts 15 | 16 | CMD ./bin/dblab-server 17 | -------------------------------------------------------------------------------- /engine/Dockerfile.swagger-ui: -------------------------------------------------------------------------------- 1 | FROM nginx:1.17-alpine 2 | COPY ./api/swagger-spec/ /usr/share/nginx/html/api/swagger-spec/ 3 | COPY ./api/swagger-ui /usr/share/nginx/html 4 | -------------------------------------------------------------------------------- /engine/api/postman/dblab.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ff4200f0-7acd-eb4f-1dee-59da8c98c313", 3 | "name": "Database Lab", 4 | "values": [ 5 | { 6 | "enabled": true, 7 | "key": "DBLAB_URL", 8 | "value": "https://url", 9 | "type": "text" 10 | }, 11 | { 12 | "enabled": true, 13 | "key": "DBLAB_VERIFY_TOKEN", 14 | "value": "secret_token", 15 | "type": "text" 16 | } 17 | ], 18 | "timestamp": 1580454458304, 19 | "_postman_variable_scope": "environment", 20 | "_postman_exported_at": "2020-01-31T09:42:37.377Z", 21 | "_postman_exported_using": "Postman/5.5.4" 22 | } 23 | -------------------------------------------------------------------------------- /engine/api/swagger-ui/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/engine/api/swagger-ui/favicon-16x16.png -------------------------------------------------------------------------------- /engine/api/swagger-ui/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/engine/api/swagger-ui/favicon-32x32.png -------------------------------------------------------------------------------- /engine/api/swagger-ui/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | overflow: -moz-scrollbars-vertical; 4 | overflow-y: scroll; 5 | } 6 | 7 | *, 8 | *:before, 9 | *:after { 10 | box-sizing: inherit; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | background: #fafafa; 16 | } 17 | -------------------------------------------------------------------------------- /engine/api/swagger-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /engine/api/swagger-ui/swagger-initializer.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | // 3 | 4 | // the following lines will be replaced by docker/configurator, when it runs in a docker-container 5 | window.ui = SwaggerUIBundle({ 6 | url: "api/swagger-spec/dblab_server_swagger.yaml", 7 | dom_id: '#swagger-ui', 8 | deepLinking: true, 9 | presets: [ 10 | SwaggerUIBundle.presets.apis, 11 | SwaggerUIStandalonePreset 12 | ], 13 | plugins: [ 14 | SwaggerUIBundle.plugins.DownloadUrl 15 | ], 16 | layout: "StandaloneLayout" 17 | }); 18 | 19 | // 20 | }; 21 | -------------------------------------------------------------------------------- /engine/cmd/cli/commands/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | package commands 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | // ActionError defines a custom type of CLI action error. 13 | type ActionError struct { 14 | err error 15 | } 16 | 17 | // NewActionError constructs a new Action error. 18 | func NewActionError(msg string) ActionError { 19 | return ActionError{err: errors.New(msg)} 20 | } 21 | 22 | // ToActionError wraps the error to a new action error. 23 | func ToActionError(err error) error { 24 | if err == nil { 25 | return nil 26 | } 27 | 28 | return ActionError{err: err} 29 | } 30 | 31 | // ActionErrorf formats according to a format specifier. 32 | func ActionErrorf(format string, args ...interface{}) error { 33 | return ActionError{ 34 | err: fmt.Errorf(format, args...), 35 | } 36 | } 37 | 38 | // Error returns an output of the action error. 39 | func (e ActionError) Error() string { 40 | return "[ERROR]: " + e.err.Error() 41 | } 42 | -------------------------------------------------------------------------------- /engine/cmd/cli/commands/instance/command_list.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | package instance 6 | 7 | import ( 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | // CommandList returns available commands for an instance management. 12 | func CommandList() []*cli.Command { 13 | return []*cli.Command{ 14 | { 15 | Name: "instance", 16 | Usage: "displays instance info", 17 | Subcommands: []*cli.Command{ 18 | { 19 | Name: "status", 20 | Usage: "display instance's status", 21 | Action: status, 22 | }, 23 | { 24 | Name: "version", 25 | Usage: "display instance's version", 26 | Action: health, 27 | }, 28 | }, 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /engine/cmd/cli/commands/snapshot/actions.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | // Package snapshot provides snapshot management commands. 6 | package snapshot 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | 12 | "github.com/urfave/cli/v2" 13 | 14 | "gitlab.com/postgres-ai/database-lab/v3/cmd/cli/commands" 15 | "gitlab.com/postgres-ai/database-lab/v3/pkg/models" 16 | ) 17 | 18 | // list runs a request to list snapshots of an instance. 19 | func list(cliCtx *cli.Context) error { 20 | dblabClient, err := commands.ClientByCLIContext(cliCtx) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | body, err := dblabClient.ListSnapshotsRaw(cliCtx.Context) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | defer func() { _ = body.Close() }() 31 | 32 | var snapshotListView []*models.SnapshotView 33 | 34 | if err := json.NewDecoder(body).Decode(&snapshotListView); err != nil { 35 | return err 36 | } 37 | 38 | commandResponse, err := json.MarshalIndent(snapshotListView, "", " ") 39 | if err != nil { 40 | return err 41 | } 42 | 43 | _, err = fmt.Fprintln(cliCtx.App.Writer, string(commandResponse)) 44 | 45 | return err 46 | } 47 | -------------------------------------------------------------------------------- /engine/cmd/cli/commands/snapshot/command_list.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | package snapshot 6 | 7 | import ( 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | // CommandList returns available commands for a snapshot management. 12 | func CommandList() []*cli.Command { 13 | return []*cli.Command{ 14 | { 15 | Name: "snapshot", 16 | Usage: "manage snapshots", 17 | Subcommands: []*cli.Command{ 18 | { 19 | Name: "list", 20 | Usage: "list all existing snapshots", 21 | Action: list, 22 | }, 23 | }, 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /engine/configs/standard/postgres/control/pg_hba.conf: -------------------------------------------------------------------------------- 1 | ## This file will replace any pg_hba.conf located in your PGDATA. 2 | 3 | local all all trust 4 | host all all 0.0.0.0/0 md5 5 | -------------------------------------------------------------------------------- /engine/configs/standard/postgres/default/11/pg_hba.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/engine/configs/standard/postgres/default/11/pg_hba.conf -------------------------------------------------------------------------------- /engine/deploy/swagger-ui.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: dblab-swagger-ui 5 | labels: 6 | app: dblab-swagger-ui 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - port: 80 11 | targetPort: 80 12 | selector: 13 | app: dblab-swagger-ui 14 | --- 15 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | name: dblab-swagger-ui 19 | labels: 20 | app: dblab-swagger-ui 21 | spec: 22 | replicas: 1 23 | selector: 24 | matchLabels: 25 | app: dblab-swagger-ui 26 | template: 27 | metadata: 28 | labels: 29 | app: dblab-swagger-ui 30 | spec: 31 | containers: 32 | - name: dblab-swagger-ui 33 | image: $TAG 34 | imagePullPolicy: Always 35 | -------------------------------------------------------------------------------- /engine/internal/cloning/wrapper.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | // Package cloning provides a cloning service. 6 | package cloning 7 | 8 | import ( 9 | "time" 10 | 11 | "gitlab.com/postgres-ai/database-lab/v3/internal/provision/resources" 12 | 13 | "gitlab.com/postgres-ai/database-lab/v3/pkg/models" 14 | ) 15 | 16 | // CloneWrapper represents a cloning service wrapper. 17 | type CloneWrapper struct { 18 | Clone *models.Clone `json:"clone"` 19 | Session *resources.Session `json:"session"` 20 | 21 | TimeCreatedAt time.Time `json:"time_created_at"` 22 | TimeStartedAt time.Time `json:"time_started_at"` 23 | } 24 | 25 | // NewCloneWrapper constructs a new CloneWrapper. 26 | func NewCloneWrapper(clone *models.Clone, createdAt time.Time) *CloneWrapper { 27 | w := &CloneWrapper{ 28 | Clone: clone, 29 | TimeCreatedAt: createdAt, 30 | } 31 | 32 | return w 33 | } 34 | 35 | // IsProtected checks if clone is protected. 36 | func (cw CloneWrapper) IsProtected() bool { 37 | return cw.Clone != nil && cw.Clone.Protected 38 | } 39 | -------------------------------------------------------------------------------- /engine/internal/diagnostic/logs_test.go: -------------------------------------------------------------------------------- 1 | package diagnostic 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | "time" 8 | 9 | "github.com/google/uuid" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestLogsCleanup(t *testing.T) { 16 | t.Parallel() 17 | now := time.Now() 18 | 19 | dir := t.TempDir() 20 | 21 | for day := 0; day <= 10; day++ { 22 | containerName, err := uuid.NewUUID() 23 | assert.NoError(t, err) 24 | 25 | name := now.AddDate(0, 0, -1*day).In(time.UTC).Format(timeFormat) 26 | 27 | err = os.MkdirAll(path.Join(dir, name, containerName.String()), 0755) 28 | require.NoError(t, err) 29 | } 30 | 31 | err := cleanupLogsDir(dir, 5) 32 | require.NoError(t, err) 33 | 34 | // list remaining directories 35 | dirList, err := os.ReadDir(dir) 36 | assert.NoError(t, err) 37 | assert.Equal(t, 5, len(dirList)) 38 | } 39 | -------------------------------------------------------------------------------- /engine/internal/embeddedui/embedded_ui_test.go: -------------------------------------------------------------------------------- 1 | package embeddedui 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestUIManagerOriginURL(t *testing.T) { 10 | testCases := []struct { 11 | host string 12 | port int 13 | result string 14 | }{ 15 | { 16 | host: "example.com", 17 | port: 2345, 18 | result: "http://example.com:2345", 19 | }, 20 | { 21 | host: "example.com", 22 | result: "http://example.com:0", 23 | }, 24 | { 25 | host: "container_host", 26 | port: 2345, 27 | result: "http://container_host:2345", 28 | }, 29 | { 30 | host: "172.168.1.1", 31 | port: 2345, 32 | result: "http://172.168.1.1:2345", 33 | }, 34 | { 35 | host: "0.0.0.0", 36 | port: 2345, 37 | result: "http://127.0.0.1:2345", 38 | }, 39 | { 40 | host: "", 41 | port: 2345, 42 | result: "http://127.0.0.1:2345", 43 | }, 44 | } 45 | 46 | for _, tc := range testCases { 47 | uiManager := UIManager{ 48 | cfg: Config{ 49 | Enabled: true, 50 | Host: tc.host, 51 | Port: tc.port, 52 | }, 53 | } 54 | 55 | require.True(t, uiManager.IsEnabled()) 56 | require.Equal(t, tc.result, uiManager.OriginURL()) 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /engine/internal/observer/queries.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | package observer 6 | 7 | import ( 8 | "fmt" 9 | ) 10 | 11 | const queryLocks = `with lock_data as ( 12 | select 13 | a.datname, 14 | l.relation::regclass, 15 | l.transactionid, 16 | l.mode, 17 | l.locktype, 18 | l.granted, 19 | a.usename, 20 | a.query, 21 | a.query_start, 22 | a.state, 23 | a.wait_event_type, 24 | a.wait_event, 25 | a.xact_start, 26 | clock_timestamp() - a.xact_start as xact_duration, 27 | a.query_start, 28 | clock_timestamp() - a.query_start as query_duration, 29 | a.state_change, 30 | clock_timestamp() - a.state_change as state_changed_ago, 31 | a.pid 32 | from pg_stat_activity a 33 | join pg_locks l on l.pid = a.pid 34 | where l.mode = 'AccessExclusiveLock' and l.locktype = 'relation' and a.application_name <> '%s' 35 | ) 36 | select row_to_json(lock_data) 37 | from lock_data 38 | where query_duration > interval '%d second' 39 | order by query_duration desc;` 40 | 41 | func buildLocksMetricQuery(exclusionApplicationName string, maxLockDurationSeconds uint64) string { 42 | return fmt.Sprintf(queryLocks, exclusionApplicationName, maxLockDurationSeconds) 43 | } 44 | -------------------------------------------------------------------------------- /engine/internal/observer/session_test.go: -------------------------------------------------------------------------------- 1 | package observer 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSessionIsFinished(t *testing.T) { 11 | s := Session{} 12 | assert.False(t, s.IsFinished()) 13 | 14 | s.FinishedAt = time.Now() 15 | assert.True(t, s.IsFinished()) 16 | } 17 | -------------------------------------------------------------------------------- /engine/internal/platform/platform_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | package platform 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestIfPersonalTokenEnabled(t *testing.T) { 14 | s := Service{} 15 | assert.Equal(t, s.IsPersonalTokenEnabled(), false) 16 | 17 | s.cfg.EnablePersonalToken = true 18 | assert.Equal(t, s.IsPersonalTokenEnabled(), true) 19 | } 20 | 21 | func TestIfOrganizationIsAllowed(t *testing.T) { 22 | s := Service{} 23 | assert.Equal(t, s.isAllowedOrganization(0), false) 24 | 25 | s.token.OrganizationID = 1 26 | assert.Equal(t, s.isAllowedOrganization(0), false) 27 | assert.Equal(t, s.isAllowedOrganization(1), true) 28 | } 29 | 30 | func TestOriginURL(t *testing.T) { 31 | s := Service{ 32 | cfg: Config{ 33 | URL: "https://example.com:2345/api/path", 34 | }, 35 | } 36 | 37 | assert.Equal(t, "https://example.com:2345", s.OriginURL()) 38 | } 39 | -------------------------------------------------------------------------------- /engine/internal/provision/databases/postgres/pgconfig/configuration_test.go: -------------------------------------------------------------------------------- 1 | package pgconfig 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestReadConfig(t *testing.T) { 12 | controlData := ` 13 | restore_command = 'wal-g wal-fetch %f %p' 14 | standby_mode = 'on' 15 | recovery_target_timeline = latest 16 | ` 17 | expected := map[string]string{ 18 | "restore_command": "wal-g wal-fetch %f %p", 19 | "standby_mode": "on", 20 | "recovery_target_timeline": "latest", 21 | } 22 | 23 | f, err := os.CreateTemp("", "readPGConfig*") 24 | require.Nil(t, err) 25 | defer func() { _ = os.Remove(f.Name()) }() 26 | 27 | err = os.WriteFile(f.Name(), []byte(controlData), 0644) 28 | require.Nil(t, err) 29 | 30 | fileConfig, err := readConfig(f.Name()) 31 | require.Nil(t, err) 32 | 33 | assert.Equal(t, len(expected), len(fileConfig)) 34 | assert.Equal(t, expected["restore_command"], fileConfig["restore_command"]) 35 | assert.Equal(t, expected["standby_mode"], fileConfig["standby_mode"]) 36 | assert.Equal(t, expected["recovery_target_timeline"], fileConfig["recovery_target_timeline"]) 37 | } 38 | -------------------------------------------------------------------------------- /engine/internal/provision/pool/fstype_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd || dragonfly || openbsd || solaris 2 | // +build darwin freebsd dragonfly openbsd solaris 3 | 4 | /* 5 | 2020 © Postgres.ai 6 | */ 7 | 8 | // Package pool provides components to work with storage pools. 9 | package pool 10 | 11 | import ( 12 | "syscall" 13 | 14 | "gitlab.com/postgres-ai/database-lab/v3/internal/provision/thinclones/lvm" 15 | ) 16 | 17 | func (pm *Manager) getFSInfo(path string) (string, error) { 18 | fs := syscall.Statfs_t{} 19 | if err := syscall.Statfs(path, &fs); err != nil { 20 | return "", err 21 | } 22 | 23 | fsType := detectFSType(fs.Fstypename[:]) 24 | if fsType == ext4 { 25 | // cannot detect LVM checking the blockDeviceTypes map. 26 | return lvm.PoolMode, nil 27 | } 28 | 29 | return fsType, nil 30 | } 31 | 32 | // detectFSType detects the filesystem type of the underlying mounted filesystem. 33 | func detectFSType(fsType []int8) string { 34 | fsTypeBytes := make([]byte, 0, len(fsType)) 35 | 36 | for _, v := range fsType { 37 | fsTypeBytes = append(fsTypeBytes, byte(v)) 38 | } 39 | 40 | return string(fsTypeBytes) 41 | } 42 | -------------------------------------------------------------------------------- /engine/internal/provision/pool/fstype_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux && !s390x && !arm && !386 2 | // +build linux,!s390x,!arm,!386 3 | 4 | /* 5 | 2020 © Postgres.ai 6 | */ 7 | 8 | // Package pool provides components to work with storage pools. 9 | package pool 10 | 11 | import ( 12 | "strconv" 13 | "syscall" 14 | 15 | "gitlab.com/postgres-ai/database-lab/v3/internal/provision/thinclones/lvm" 16 | "gitlab.com/postgres-ai/database-lab/v3/internal/provision/thinclones/zfs" 17 | ) 18 | 19 | var fsTypeToString = map[string]string{ 20 | "ef53": ext4, 21 | "2fc12fc1": zfs.PoolMode, 22 | } 23 | 24 | func (pm *Manager) getFSInfo(path string) (string, error) { 25 | fs := syscall.Statfs_t{} 26 | if err := syscall.Statfs(path, &fs); err != nil { 27 | return "", err 28 | } 29 | 30 | fsType := detectFSType(fs.Type) 31 | if fsType == ext4 { 32 | // cannot detect LVM checking the blockDeviceTypes map. 33 | return lvm.PoolMode, nil 34 | } 35 | 36 | return fsType, nil 37 | } 38 | 39 | // detectFSType detects the filesystem type of the underlying mounted filesystem. 40 | func detectFSType(fsType int64) string { 41 | return fsTypeToString[strconv.FormatInt(fsType, 16)] 42 | } 43 | -------------------------------------------------------------------------------- /engine/internal/provision/pool/fstype_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | /* 5 | 2020 © Postgres.ai 6 | */ 7 | 8 | // Package pool provides components to work with storage pools. 9 | package pool 10 | 11 | func (pm *Manager) getFSInfo(path string) (string, error) { 12 | // Not supported for windows. 13 | return "", nil 14 | } 15 | -------------------------------------------------------------------------------- /engine/internal/provision/port_checker.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | package provision 6 | 7 | import ( 8 | "net" 9 | "strconv" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type portChecker interface { 15 | checkPortAvailability(host string, port uint) error 16 | } 17 | 18 | type localPortChecker struct{} 19 | 20 | func (c *localPortChecker) checkPortAvailability(host string, port uint) error { 21 | addr := net.JoinHostPort(host, strconv.Itoa(int(port))) 22 | 23 | conn, err := net.DialTimeout("tcp", addr, portCheckingTimeout) 24 | if conn != nil { 25 | _ = conn.Close() 26 | } 27 | 28 | if err == nil { 29 | return errors.New("port already in use") 30 | } 31 | 32 | if opErr, ok := err.(*net.OpError); ok && 33 | opErr != nil && opErr.Err != nil && opErr.Err.Error() == "connect: connection refused" { 34 | return nil 35 | } 36 | 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /engine/internal/provision/provision.go: -------------------------------------------------------------------------------- 1 | /* 2 | Provision wrapper 3 | 4 | 2019-2020 © Postgres.ai 5 | */ 6 | 7 | // Package provision provides an interface to provision Database Lab clones. 8 | package provision 9 | 10 | // NoRoomError defines a specific error type. 11 | type NoRoomError struct { 12 | msg string 13 | } 14 | 15 | // NewNoRoomError instances a new NoRoomError. 16 | func NewNoRoomError(errorMessage string) error { 17 | return &NoRoomError{msg: errorMessage} 18 | } 19 | 20 | func (e *NoRoomError) Error() string { 21 | // TODO(anatoly): Change message. 22 | return "session cannot be started because there is no room: " + e.msg 23 | } 24 | -------------------------------------------------------------------------------- /engine/internal/provision/thinclones/manager.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | // Package thinclones provides an interface to work different thin-clone managers. 6 | package thinclones 7 | 8 | import ( 9 | "fmt" 10 | ) 11 | 12 | // SnapshotExistsError defines an error when snapshot already exists. 13 | type SnapshotExistsError struct { 14 | name string 15 | } 16 | 17 | // NewSnapshotExistsError creates a new SnapshotExistsError. 18 | func NewSnapshotExistsError(name string) *SnapshotExistsError { 19 | return &SnapshotExistsError{name: name} 20 | } 21 | 22 | // Error prints a message describing SnapshotExistsError. 23 | func (e *SnapshotExistsError) Error() string { 24 | return fmt.Sprintf(`snapshot %s already exists`, e.name) 25 | } 26 | -------------------------------------------------------------------------------- /engine/internal/provision/thinclones/zfs/snapshots_filter.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 © Postgres.ai 3 | */ 4 | 5 | package zfs 6 | 7 | import ( 8 | "strings" 9 | ) 10 | 11 | type dsType string 12 | 13 | const ( 14 | snapshotType dsType = "snapshot" 15 | fileSystemType dsType = "filesystem" 16 | ) 17 | 18 | type snapshotFields []string 19 | 20 | type snapshotSorting []string 21 | 22 | type snapshotFilter struct { 23 | fields snapshotFields 24 | sorting snapshotSorting 25 | dsType dsType 26 | pool string 27 | } 28 | 29 | var defaultFields = snapshotFields{ 30 | "name", 31 | "used", 32 | "mountpoint", 33 | "compressratio", 34 | "available", 35 | "type", 36 | "origin", 37 | "creation", 38 | "referenced", 39 | "logicalreferenced", 40 | "logicalused", 41 | "usedbysnapshots", 42 | "usedbychildren", 43 | dataStateAtLabel, 44 | } 45 | 46 | var defaultSorting = snapshotSorting{ 47 | "-S " + dataStateAtLabel, 48 | "-S creation", 49 | } 50 | 51 | func buildListCommand(filter snapshotFilter) string { 52 | cmdComponents := []string{ 53 | "zfs list", 54 | "-po", strings.Join(filter.fields, ","), 55 | strings.Join(filter.sorting, " "), 56 | "-t", string(filter.dsType), 57 | } 58 | 59 | if filter.pool != "" { 60 | cmdComponents = append(cmdComponents, "-r", filter.pool) 61 | } 62 | 63 | return strings.Join(cmdComponents, " ") 64 | } 65 | -------------------------------------------------------------------------------- /engine/internal/provision/thinclones/zfs/snapshots_filter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 © Postgres.ai 3 | */ 4 | 5 | package zfs 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestListCommand(t *testing.T) { 14 | testCases := []struct { 15 | filter snapshotFilter 16 | expectedCommand string 17 | }{ 18 | { 19 | filter: snapshotFilter{ 20 | fields: []string{"name", "creation"}, 21 | sorting: []string{"-S creation"}, 22 | dsType: "snapshot", 23 | pool: "test_pool", 24 | }, 25 | expectedCommand: "zfs list -po name,creation -S creation -t snapshot -r test_pool", 26 | }, 27 | { 28 | filter: snapshotFilter{ 29 | fields: []string{"name", "creation", "dblab:datastateat"}, 30 | sorting: []string{"-S creation", "-S dblab:datastateat"}, 31 | dsType: "filesystem", 32 | }, 33 | expectedCommand: "zfs list -po name,creation,dblab:datastateat -S creation -S dblab:datastateat -t filesystem", 34 | }, 35 | } 36 | 37 | for _, tc := range testCases { 38 | listCmd := buildListCommand(tc.filter) 39 | assert.Equal(t, tc.expectedCommand, listCmd) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /engine/internal/retrieval/components/components.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | // Package components provides the key components of data retrieval. 6 | package components 7 | 8 | import ( 9 | "context" 10 | 11 | "gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/config" 12 | "gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools/activity" 13 | ) 14 | 15 | // JobBuilder builds jobs. 16 | type JobBuilder interface { 17 | // BuildJob builds retrieval jobs. 18 | BuildJob(jobConfig config.JobConfig) (JobRunner, error) 19 | } 20 | 21 | // JobRunner performs a job. 22 | type JobRunner interface { 23 | // Name returns a job name. 24 | Name() string 25 | 26 | // Reload reloads job configuration. 27 | Reload(cfg map[string]interface{}) error 28 | 29 | // Run starts a job. 30 | Run(ctx context.Context) error 31 | 32 | // ReportActivity reports the current job activity. 33 | ReportActivity(context.Context) (*activity.Activity, error) 34 | } 35 | -------------------------------------------------------------------------------- /engine/internal/retrieval/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | // Package config contains configuration options of the data retrieval. 6 | package config 7 | 8 | import ( 9 | "github.com/docker/docker/client" 10 | 11 | "gitlab.com/postgres-ai/database-lab/v3/internal/provision/resources" 12 | "gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/dbmarker" 13 | ) 14 | 15 | // Config describes of data retrieval jobs. 16 | type Config struct { 17 | Refresh *Refresh `yaml:"refresh"` 18 | Jobs []string `yaml:"jobs,flow"` 19 | JobsSpec map[string]JobSpec `yaml:"spec"` 20 | } 21 | 22 | // Refresh describes full-refresh options. 23 | type Refresh struct { 24 | Timetable string `yaml:"timetable"` 25 | SkipStartRefresh bool `yaml:"skipStartRefresh"` 26 | } 27 | 28 | // JobSpec contains details about a job. 29 | type JobSpec struct { 30 | Name string `yaml:"name"` 31 | Options map[string]interface{} `yaml:"options"` 32 | } 33 | 34 | // JobConfig describes a job configuration. 35 | type JobConfig struct { 36 | Spec JobSpec 37 | Docker *client.Client 38 | Marker *dbmarker.Marker 39 | FSPool *resources.Pool 40 | } 41 | -------------------------------------------------------------------------------- /engine/internal/retrieval/engine/engine.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | // Package engine provides different engines. 6 | package engine 7 | 8 | import ( 9 | "errors" 10 | 11 | "gitlab.com/postgres-ai/database-lab/v3/internal/provision/pool" 12 | "gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/components" 13 | "gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres" 14 | "gitlab.com/postgres-ai/database-lab/v3/internal/telemetry" 15 | 16 | "gitlab.com/postgres-ai/database-lab/v3/pkg/config/global" 17 | ) 18 | 19 | // JobBuilder provides a new job builder. 20 | func JobBuilder(globalCfg *global.Config, engineProps *global.EngineProps, cloneManager pool.FSManager, 21 | tm *telemetry.Agent) (components.JobBuilder, error) { 22 | switch globalCfg.Engine { 23 | case postgres.EngineType: 24 | return postgres.NewJobBuilder(globalCfg, engineProps, cloneManager, tm), nil 25 | 26 | default: 27 | return nil, errors.New("failed to get engine") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /engine/internal/retrieval/engine/postgres/logical/archive_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 © Postgres.ai 3 | */ 4 | 5 | package logical 6 | 7 | import ( 8 | "path/filepath" 9 | ) 10 | 11 | type compressionType string 12 | 13 | const ( 14 | noCompression compressionType = "no" 15 | gzipCompression compressionType = "gzip" 16 | bzip2Compression compressionType = "bzip2" 17 | ) 18 | 19 | // getReadingArchiveCommand chooses command to read dump file. 20 | func getReadingArchiveCommand(compressionType compressionType) string { 21 | switch compressionType { 22 | case gzipCompression: 23 | return "gunzip -c" 24 | 25 | case bzip2Compression: 26 | return "bunzip2 -c" 27 | 28 | default: 29 | return "cat" 30 | } 31 | } 32 | 33 | // getCompressionType returns archive type based on filename extension. 34 | func getCompressionType(filename string) compressionType { 35 | switch filepath.Ext(filename) { 36 | case ".gz": 37 | return gzipCompression 38 | 39 | case ".bz2": 40 | return bzip2Compression 41 | 42 | default: 43 | return noCompression 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /engine/internal/retrieval/engine/postgres/logical/dump_default.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | package logical 6 | 7 | import ( 8 | "context" 9 | 10 | "gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools/db" 11 | ) 12 | 13 | type defaultDumper struct { 14 | c *Connection 15 | } 16 | 17 | func newDefaultDumper() *defaultDumper { 18 | return &defaultDumper{} 19 | } 20 | 21 | func (d *defaultDumper) GetCmdEnvVariables() []string { 22 | return []string{} 23 | } 24 | 25 | func (d *defaultDumper) SetConnectionOptions(_ context.Context, c *Connection) error { 26 | d.c = c 27 | return nil 28 | } 29 | 30 | func (d *defaultDumper) GetDatabaseListQuery(username string) string { 31 | return db.GetDatabaseListQuery(username) 32 | } 33 | -------------------------------------------------------------------------------- /engine/internal/retrieval/engine/postgres/logical/logical.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | // Package logical provides jobs for logical initial operations. 6 | package logical 7 | 8 | import ( 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/docker/docker/api/types/mount" 13 | ) 14 | 15 | func buildAnalyzeCommand(conn Connection, parallelJobs int) []string { 16 | analyzeCmd := []string{ 17 | "vacuumdb", 18 | "--analyze", 19 | "--jobs", strconv.Itoa(parallelJobs), 20 | "--username", conn.Username, 21 | "--all", 22 | } 23 | 24 | return analyzeCmd 25 | } 26 | 27 | func isAlreadyMounted(mounts []mount.Mount, dir string) bool { 28 | dir = strings.Trim(dir, "/") 29 | 30 | for _, mountPoint := range mounts { 31 | if strings.Trim(mountPoint.Source, "/") == dir { 32 | return true 33 | } 34 | } 35 | 36 | return false 37 | } 38 | -------------------------------------------------------------------------------- /engine/internal/retrieval/engine/postgres/physical/custom_test.go: -------------------------------------------------------------------------------- 1 | package physical 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCustomRecoveryConfig(t *testing.T) { 10 | customTool := newCustomTool(customOptions{ 11 | RestoreCommand: "pg_basebackup -X stream -D dataDirectory", 12 | }) 13 | 14 | recoveryConfig := customTool.GetRecoveryConfig(11.7) 15 | expectedResponse11 := map[string]string{ 16 | "restore_command": "pg_basebackup -X stream -D dataDirectory", 17 | "recovery_target_timeline": "latest", 18 | } 19 | assert.Equal(t, expectedResponse11, recoveryConfig) 20 | 21 | recoveryConfig = customTool.GetRecoveryConfig(12.3) 22 | expectedResponse12 := map[string]string{ 23 | "restore_command": "pg_basebackup -X stream -D dataDirectory", 24 | } 25 | assert.Equal(t, expectedResponse12, recoveryConfig) 26 | } 27 | -------------------------------------------------------------------------------- /engine/internal/retrieval/engine/postgres/physical/physical_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | package physical 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestInitParamsExtraction(t *testing.T) { 17 | controlDataOutput := bytes.NewBufferString(` 18 | wal_level setting: logical 19 | wal_log_hints setting: on 20 | max_connections setting: 500 21 | max_worker_processes setting: 8 22 | max_prepared_xacts setting: 3 23 | max_locks_per_xact setting: 128 24 | track_commit_timestamp setting: off 25 | max_wal_senders setting: 15 26 | `) 27 | 28 | expectedSettings := map[string]string{ 29 | "max_connections": "500", 30 | "max_locks_per_transaction": "128", 31 | "max_prepared_transactions": "3", 32 | "max_worker_processes": "8", 33 | "track_commit_timestamp": "off", 34 | "max_wal_senders": "15", 35 | } 36 | 37 | settings, err := extractControlDataParams(context.Background(), controlDataOutput) 38 | 39 | require.Nil(t, err) 40 | assert.EqualValues(t, settings, expectedSettings) 41 | } 42 | -------------------------------------------------------------------------------- /engine/internal/retrieval/engine/postgres/physical/wal_g_test.go: -------------------------------------------------------------------------------- 1 | package physical 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestWALGRecoveryConfig(t *testing.T) { 10 | walg := newWALG(nil, "dataDir", walgOptions{}) 11 | 12 | recoveryConfig := walg.GetRecoveryConfig(11.7) 13 | expectedResponse11 := map[string]string{ 14 | "restore_command": "wal-g wal-fetch %f %p", 15 | "recovery_target_timeline": "latest", 16 | } 17 | assert.Equal(t, expectedResponse11, recoveryConfig) 18 | 19 | recoveryConfig = walg.GetRecoveryConfig(12.3) 20 | expectedResponse12 := map[string]string{ 21 | "restore_command": "wal-g wal-fetch %f %p", 22 | } 23 | assert.Equal(t, expectedResponse12, recoveryConfig) 24 | } 25 | 26 | func TestWALGVersionParse(t *testing.T) { 27 | version, err := parseWalGVersion("wal-g version v2.0.0\t1eb88a5\t2022.05.20_10:45:57\tPostgreSQL") 28 | assert.NoError(t, err) 29 | assert.NotEmpty(t, version) 30 | assert.Equal(t, "v2.0.0", version) 31 | } 32 | -------------------------------------------------------------------------------- /engine/internal/retrieval/engine/postgres/snapshot/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | package snapshot 6 | 7 | type skipSnapshotErr struct { 8 | message string 9 | } 10 | 11 | func newSkipSnapshotErr(message string) *skipSnapshotErr { 12 | return &skipSnapshotErr{message: message} 13 | } 14 | 15 | func (e *skipSnapshotErr) Error() string { 16 | return e.message 17 | } 18 | -------------------------------------------------------------------------------- /engine/internal/retrieval/engine/postgres/snapshot/snapshot.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | // Package snapshot provides components for preparing initial snapshots. 6 | package snapshot 7 | 8 | import ( 9 | "github.com/pkg/errors" 10 | 11 | "gitlab.com/postgres-ai/database-lab/v3/internal/provision/runners" 12 | "gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/dbmarker" 13 | "gitlab.com/postgres-ai/database-lab/v3/pkg/log" 14 | ) 15 | 16 | func extractDataStateAt(dbMarker *dbmarker.Marker) string { 17 | dbMark, err := dbMarker.GetConfig() 18 | if err != nil { 19 | log.Msg("Cannot retrieve dataStateAt from DBMarker config:", err) 20 | return "" 21 | } 22 | 23 | return dbMark.DataStateAt 24 | } 25 | 26 | func runPreprocessingScript(preprocessingScript string) error { 27 | commandOutput, err := runners.NewLocalRunner(false).Run(preprocessingScript) 28 | if err != nil { 29 | return errors.Wrap(err, "failed to run custom script") 30 | } 31 | 32 | log.Msg(commandOutput) 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /engine/internal/retrieval/engine/postgres/tools/activity/activity.go: -------------------------------------------------------------------------------- 1 | // Package activity observes activities of the data retrieval process. 2 | package activity 3 | 4 | // Activity represents job activity. 5 | type Activity struct { 6 | Source []PGEvent 7 | Target []PGEvent 8 | } 9 | 10 | // PGEvent represents pg_stat_activity event. 11 | type PGEvent struct { 12 | User string 13 | Duration float64 14 | Query string 15 | WaitEventType string 16 | WaitEvent string 17 | } 18 | -------------------------------------------------------------------------------- /engine/internal/retrieval/engine/postgres/tools/defaults/defaults.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | // Package defaults contains default values. 6 | package defaults 7 | 8 | const ( 9 | // Port defines a default port. 10 | Port = 5432 11 | 12 | // Username defines a default user name. 13 | Username = "postgres" 14 | 15 | // DBName defines a default database name. 16 | DBName = "postgres" 17 | 18 | // PGVersion12 defines the PostgreSQL 12 version. 19 | PGVersion12 = 12 20 | ) 21 | -------------------------------------------------------------------------------- /engine/internal/retrieval/options/options.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | // Package options provides helpers to process retrieval options. 6 | package options 7 | 8 | import ( 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | // Unmarshal converts configuration to specific options. 13 | func Unmarshal(in, out interface{}) error { 14 | // TODO: Parse default yaml values in tags. 15 | b, err := yaml.Marshal(in) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | return yaml.Unmarshal(b, out) 21 | } 22 | -------------------------------------------------------------------------------- /engine/internal/retrieval/refresh_errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 © Postgres.ai 3 | */ 4 | 5 | package retrieval 6 | 7 | // SkipRefreshingError defines an error when data refreshing is skipped. 8 | type SkipRefreshingError struct { 9 | msg string 10 | } 11 | 12 | // NewSkipRefreshingError creates a new SkipRefreshingError. 13 | func NewSkipRefreshingError(msg string) *SkipRefreshingError { 14 | return &SkipRefreshingError{ 15 | msg: msg, 16 | } 17 | } 18 | 19 | // Error returns error message. 20 | func (e *SkipRefreshingError) Error() string { 21 | return e.msg 22 | } 23 | -------------------------------------------------------------------------------- /engine/internal/runci/source/source.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 © Postgres.ai 3 | */ 4 | 5 | // Package source provides a tools to use version control systems. 6 | package source 7 | 8 | import ( 9 | "context" 10 | ) 11 | 12 | const ( 13 | // RepoDir defines a directory to clone and extract repository. 14 | RepoDir = "/tmp/ci_checker" 15 | ) 16 | 17 | // Config describes the configuration of the plugged version control system. 18 | type Config struct { 19 | Type string `yaml:"type"` 20 | Token string `yaml:"token"` 21 | } 22 | 23 | // Provider declares code provider interface. 24 | type Provider interface { 25 | Download(ctx context.Context, opts Opts, output string) error 26 | Extract(file string) (sourceCodeDir string, err error) 27 | } 28 | 29 | // Opts declares repository options. 30 | type Opts struct { 31 | Owner string `json:"owner"` 32 | Repo string `json:"repo"` 33 | Ref string `json:"ref"` 34 | Branch string `json:"branch"` 35 | BranchLink string `json:"branch_link"` 36 | Commit string `json:"commit"` 37 | CommitLink string `json:"commit_link"` 38 | RequestLink string `json:"request_link"` 39 | DiffLink string `json:"diff_link"` 40 | } 41 | -------------------------------------------------------------------------------- /engine/internal/srv/api/errors_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | package api 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "gitlab.com/postgres-ai/database-lab/v3/pkg/models" 13 | ) 14 | 15 | func TestErrorCodeStatuses(t *testing.T) { 16 | testCases := []struct { 17 | error models.ErrorCode 18 | code int 19 | }{ 20 | { 21 | error: "BAD_REQUEST", 22 | code: 400, 23 | }, 24 | { 25 | error: "UNAUTHORIZED", 26 | code: 401, 27 | }, 28 | { 29 | error: "NOT_FOUND", 30 | code: 404, 31 | }, 32 | { 33 | error: "INTERNAL_ERROR", 34 | code: 500, 35 | }, 36 | { 37 | error: "UNKNOWN_ERROR", 38 | code: 500, 39 | }, 40 | } 41 | 42 | for _, tc := range testCases { 43 | errorCode := toStatusCode(models.Error{Code: tc.error}) 44 | 45 | assert.Equal(t, tc.code, errorCode) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /engine/internal/srv/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 © Postgres.ai 3 | */ 4 | 5 | // Package config contains configuration options of HTTP server. 6 | package config 7 | 8 | // Config provides configuration management via DLE API 9 | type Config struct { 10 | VerificationToken string `yaml:"verificationToken" json:"-"` 11 | Host string `yaml:"host"` 12 | Port uint `yaml:"port"` 13 | DisableConfigModification bool `yaml:"disableConfigModification" json:"-"` 14 | } 15 | -------------------------------------------------------------------------------- /engine/internal/srv/config_test.go: -------------------------------------------------------------------------------- 1 | package srv 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestCustomOptions(t *testing.T) { 10 | testCases := []struct { 11 | customOptions []interface{} 12 | expectedResult error 13 | }{ 14 | { 15 | customOptions: []interface{}{"--verbose"}, 16 | expectedResult: nil, 17 | }, 18 | { 19 | customOptions: []interface{}{"--exclude-scheme=test_scheme"}, 20 | expectedResult: nil, 21 | }, 22 | { 23 | customOptions: []interface{}{`--exclude-scheme="test_scheme"`}, 24 | expectedResult: nil, 25 | }, 26 | { 27 | customOptions: []interface{}{"--table=$(echo 'test')"}, 28 | expectedResult: errInvalidOption, 29 | }, 30 | { 31 | customOptions: []interface{}{"--table=test&table"}, 32 | expectedResult: errInvalidOption, 33 | }, 34 | { 35 | customOptions: []interface{}{5}, 36 | expectedResult: errInvalidOptionType, 37 | }, 38 | } 39 | 40 | for _, tc := range testCases { 41 | validationResult := validateCustomOptions(tc.customOptions) 42 | 43 | require.ErrorIs(t, validationResult, tc.expectedResult) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /engine/internal/srv/mw/logging.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | package mw 6 | 7 | import ( 8 | "net/http" 9 | 10 | "gitlab.com/postgres-ai/database-lab/v3/pkg/log" 11 | ) 12 | 13 | // Logging logs the incoming request. 14 | func Logging(next http.Handler) http.Handler { 15 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | log.Msg("-> ", r.Method, r.RequestURI) 17 | next.ServeHTTP(w, r) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /engine/internal/srv/ws/ping.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gorilla/websocket" 7 | 8 | "gitlab.com/postgres-ai/database-lab/v3/pkg/log" 9 | ) 10 | 11 | const ( 12 | // Time allowed to write a message to the peer. 13 | writeWait = 10 * time.Second 14 | 15 | // Time allowed to read the next pong message from the peer. 16 | pongWait = 60 * time.Second 17 | 18 | // Send pings to peer with this period. Must be less than pongWait. 19 | pingPeriod = (pongWait * 9) / 10 //nolint 20 | ) 21 | 22 | // Ping sends control messages. 23 | func Ping(ws *websocket.Conn, done chan struct{}) { 24 | ticker := time.NewTicker(pingPeriod) 25 | defer ticker.Stop() 26 | 27 | for { 28 | select { 29 | case <-ticker.C: 30 | if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil { 31 | log.Err(err) 32 | } 33 | 34 | case <-done: 35 | return 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /engine/internal/validator/validator.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | // Package validator provides a validation service. 6 | package validator 7 | 8 | import ( 9 | "fmt" 10 | "strings" 11 | 12 | "github.com/pkg/errors" 13 | passwordvalidator "github.com/wagslane/go-password-validator" 14 | 15 | "gitlab.com/postgres-ai/database-lab/v3/pkg/client/dblabapi/types" 16 | ) 17 | 18 | const minEntropyBits = 60 19 | 20 | // Service provides a validation service. 21 | type Service struct { 22 | } 23 | 24 | // ValidateCloneRequest validates a clone request. 25 | func (v Service) ValidateCloneRequest(cloneRequest *types.CloneCreateRequest) error { 26 | if cloneRequest.DB == nil { 27 | return errors.New("missing both DB username and password") 28 | } 29 | 30 | if cloneRequest.DB.Username == "" { 31 | return errors.New("missing DB username") 32 | } 33 | 34 | if cloneRequest.DB.Password == "" { 35 | return errors.New("missing DB password") 36 | } 37 | 38 | if cloneRequest.ID != "" && strings.Contains(cloneRequest.ID, "/") { 39 | return errors.New("Clone ID cannot contain slash ('/'). Please choose another ID") 40 | } 41 | 42 | if err := passwordvalidator.Validate(cloneRequest.DB.Password, minEntropyBits); err != nil { 43 | return fmt.Errorf("password validation: %w", err) 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /engine/pkg/client/dblabapi/snapshot.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | package dblabapi 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "io" 11 | "net/http" 12 | 13 | "github.com/pkg/errors" 14 | 15 | "gitlab.com/postgres-ai/database-lab/v3/pkg/models" 16 | ) 17 | 18 | // ListSnapshots provides a snapshot list. 19 | func (c *Client) ListSnapshots(ctx context.Context) ([]*models.Snapshot, error) { 20 | body, err := c.ListSnapshotsRaw(ctx) 21 | if err != nil { 22 | return nil, errors.Wrap(err, "failed to get response") 23 | } 24 | 25 | defer func() { _ = body.Close() }() 26 | 27 | var snapshots []*models.Snapshot 28 | 29 | if err := json.NewDecoder(body).Decode(&snapshots); err != nil { 30 | return nil, errors.Wrap(err, "failed to get response") 31 | } 32 | 33 | return snapshots, nil 34 | } 35 | 36 | // ListSnapshotsRaw provides a snapshot list in raw format. 37 | func (c *Client) ListSnapshotsRaw(ctx context.Context) (io.ReadCloser, error) { 38 | u := c.URL("/snapshots") 39 | 40 | request, err := http.NewRequest(http.MethodGet, u.String(), nil) 41 | if err != nil { 42 | return nil, errors.Wrap(err, "failed to make a request") 43 | } 44 | 45 | response, err := c.Do(ctx, request) 46 | if err != nil { 47 | return nil, errors.Wrap(err, "failed to get response") 48 | } 49 | 50 | return response.Body, nil 51 | } 52 | -------------------------------------------------------------------------------- /engine/pkg/client/dblabapi/types/clone.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | // Package types provides request structures for Database Lab HTTP API. 6 | package types 7 | 8 | // CloneCreateRequest represents clone params of a create request. 9 | type CloneCreateRequest struct { 10 | ID string `json:"id"` 11 | Protected bool `json:"protected"` 12 | DB *DatabaseRequest `json:"db"` 13 | Snapshot *SnapshotCloneFieldRequest `json:"snapshot"` 14 | ExtraConf map[string]string `json:"extra_conf"` 15 | } 16 | 17 | // CloneUpdateRequest represents params of an update request. 18 | type CloneUpdateRequest struct { 19 | Protected bool `json:"protected"` 20 | } 21 | 22 | // DatabaseRequest represents database params of a clone request. 23 | type DatabaseRequest struct { 24 | Username string `json:"username"` 25 | Password string `json:"password"` 26 | Restricted bool `json:"restricted"` 27 | DBName string `json:"db_name"` 28 | } 29 | 30 | // SnapshotCloneFieldRequest represents snapshot params of a create request. 31 | type SnapshotCloneFieldRequest struct { 32 | ID string `json:"id"` 33 | } 34 | 35 | // ResetCloneRequest represents snapshot params of a reset request. 36 | type ResetCloneRequest struct { 37 | SnapshotID string `json:"snapshotID"` 38 | Latest bool `json:"latest"` 39 | } 40 | -------------------------------------------------------------------------------- /engine/pkg/client/dblabapi/types/observation.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | package types 6 | 7 | // StartObservationRequest represents a request for the start observation endpoint. 8 | type StartObservationRequest struct { 9 | CloneID string `json:"clone_id"` 10 | Config Config `json:"config"` 11 | Tags map[string]string `json:"tags"` 12 | DBName string `json:"db_name"` 13 | } 14 | 15 | // Config defines configuration options for observer. 16 | type Config struct { 17 | ObservationInterval uint64 `json:"observation_interval"` 18 | MaxLockDuration uint64 `json:"max_lock_duration"` 19 | MaxDuration uint64 `json:"max_duration"` 20 | } 21 | 22 | // StopObservationRequest represents a request for the stop observation endpoint. 23 | type StopObservationRequest struct { 24 | CloneID string `json:"clone_id"` 25 | OverallError bool `json:"overall_error"` 26 | } 27 | -------------------------------------------------------------------------------- /engine/pkg/client/platform/token.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | // Package platform provides the Platform API client. 6 | package platform 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | 12 | "github.com/pkg/errors" 13 | 14 | "gitlab.com/postgres-ai/database-lab/v3/pkg/log" 15 | ) 16 | 17 | // TokenCheckRequest represents a token checking request. 18 | type TokenCheckRequest struct { 19 | Token string `json:"token"` 20 | } 21 | 22 | // TokenCheckResponse represents response of a token checking request. 23 | type TokenCheckResponse struct { 24 | APIResponse 25 | OrganizationID uint `json:"org_id"` 26 | Personal bool `json:"is_personal"` 27 | } 28 | 29 | // CheckPlatformToken makes an HTTP request to check the Platform Access Token. 30 | func (p *Client) CheckPlatformToken(ctx context.Context, request TokenCheckRequest) (TokenCheckResponse, error) { 31 | respData := TokenCheckResponse{} 32 | 33 | if err := p.doPost(ctx, "/rpc/dblab_token_check", request, &respData); err != nil { 34 | return respData, errors.Wrap(err, "failed to post request") 35 | } 36 | 37 | if respData.Code != "" || respData.Message != "" { 38 | log.Dbg(fmt.Sprintf("Unsuccessful response given. Request: %v", request)) 39 | 40 | return respData, errors.Errorf("error: %v", respData) 41 | } 42 | 43 | return respData, nil 44 | } 45 | -------------------------------------------------------------------------------- /engine/pkg/models/clone.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | package models 6 | 7 | // Clone defines a clone model. 8 | type Clone struct { 9 | ID string `json:"id"` 10 | Snapshot *Snapshot `json:"snapshot"` 11 | Protected bool `json:"protected"` 12 | DeleteAt *LocalTime `json:"deleteAt"` 13 | CreatedAt *LocalTime `json:"createdAt"` 14 | Status Status `json:"status"` 15 | DB Database `json:"db"` 16 | Metadata CloneMetadata `json:"metadata"` 17 | } 18 | 19 | // CloneMetadata contains fields describing a clone model. 20 | type CloneMetadata struct { 21 | CloneDiffSize uint64 `json:"cloneDiffSize"` 22 | LogicalSize uint64 `json:"logicalSize"` 23 | CloningTime float64 `json:"cloningTime"` 24 | MaxIdleMinutes uint `json:"maxIdleMinutes"` 25 | } 26 | 27 | // CloneView represents a view of clone model. 28 | type CloneView struct { 29 | *Clone 30 | Snapshot *SnapshotView `json:"snapshot"` 31 | Metadata CloneMetadataView `json:"metadata"` 32 | } 33 | 34 | // CloneMetadataView represents a view of clone metadata. 35 | type CloneMetadataView struct { 36 | *CloneMetadata 37 | CloneDiffSize Size `json:"cloneDiffSize"` 38 | LogicalSize Size `json:"logicalSize"` 39 | } 40 | -------------------------------------------------------------------------------- /engine/pkg/models/database.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | package models 6 | 7 | // Database defines clone database parameters. 8 | type Database struct { 9 | ConnStr string `json:"connStr"` 10 | Host string `json:"host"` 11 | Port string `json:"port"` 12 | Username string `json:"username"` 13 | Password string `json:"password"` 14 | DBName string `json:"dbName"` 15 | } 16 | -------------------------------------------------------------------------------- /engine/pkg/models/error.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | // Package models provides Database Lab struct. 6 | package models 7 | 8 | // ErrorCode defines a response error type. 9 | type ErrorCode string 10 | 11 | // ErrCode constants define a response error codes. 12 | const ( 13 | ErrCodeInternal ErrorCode = "INTERNAL_ERROR" 14 | ErrCodeBadRequest ErrorCode = "BAD_REQUEST" 15 | ErrCodeUnauthorized ErrorCode = "UNAUTHORIZED" 16 | ErrCodeNotFound ErrorCode = "NOT_FOUND" 17 | ) 18 | 19 | // Error struct represents a response error. 20 | type Error struct { 21 | Code ErrorCode `json:"code"` 22 | Message string `json:"message"` 23 | } 24 | 25 | var _ error = &Error{} 26 | 27 | // New creates ClientError instance with given code and message. 28 | func New(code ErrorCode, message string) *Error { 29 | return &Error{ 30 | Code: code, 31 | Message: message, 32 | } 33 | } 34 | 35 | // Error prints an error message. 36 | func (e Error) Error() string { 37 | return e.Message 38 | } 39 | -------------------------------------------------------------------------------- /engine/pkg/models/fs.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | package models 6 | 7 | import ( 8 | "fmt" 9 | "math/big" 10 | 11 | "github.com/dustin/go-humanize" 12 | ) 13 | 14 | // FileSystem describes state of a file system. 15 | type FileSystem struct { 16 | Mode string `json:"mode"` 17 | Size uint64 `json:"size"` 18 | Free uint64 `json:"free"` 19 | Used uint64 `json:"used"` 20 | DataSize uint64 `json:"dataSize"` 21 | UsedBySnapshots uint64 `json:"usedBySnapshots"` 22 | UsedByClones uint64 `json:"usedByClones"` 23 | CompressRatio float64 `json:"compressRatio"` 24 | } 25 | 26 | // FileSystemView describes a view of file system state. 27 | type FileSystemView struct { 28 | *FileSystem 29 | Size Size `json:"size"` 30 | Free Size `json:"free"` 31 | Used Size `json:"used"` 32 | DataSize Size `json:"dataSize"` 33 | UsedBySnapshots Size `json:"usedBySnapshots"` 34 | UsedByClones Size `json:"usedByClones"` 35 | } 36 | 37 | // Size describes amount of disk space. 38 | type Size uint64 39 | 40 | // MarshalJSON marshals the Size struct. 41 | func (s Size) MarshalJSON() ([]byte, error) { 42 | humanReadableSize := humanize.BigIBytes(big.NewInt(int64(s))) 43 | return []byte(fmt.Sprintf("%q", humanReadableSize)), nil 44 | } 45 | -------------------------------------------------------------------------------- /engine/pkg/models/local_time.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2022 © Postgres.ai 3 | */ 4 | 5 | package models 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "time" 11 | ) 12 | 13 | const legacyFormat = "2006-01-02 15:04:05 UTC" 14 | 15 | // LocalTime defines a type of time with custom marshalling depends on locale. 16 | type LocalTime struct { 17 | time.Time 18 | } 19 | 20 | // NewLocalTime creates a new instance of LocalTime. 21 | func NewLocalTime(t time.Time) *LocalTime { 22 | return &LocalTime{Time: t} 23 | } 24 | 25 | // UnmarshalJSON un-marshals LocalTime. 26 | func (t *LocalTime) UnmarshalJSON(data []byte) error { 27 | localTime := bytes.Trim(data, "\"") 28 | 29 | if len(localTime) == 0 { 30 | return nil 31 | } 32 | 33 | parsedTime, err := time.Parse(time.RFC3339, string(localTime)) 34 | if err != nil { 35 | // Try to parse the legacy format to keep backward compatibility when restore clone sessions. 36 | parsedTime, err = time.Parse(legacyFormat, string(localTime)) 37 | if err != nil { 38 | return err 39 | } 40 | } 41 | 42 | t.Time = parsedTime 43 | 44 | return nil 45 | } 46 | 47 | // MarshalJSON marshals LocalTime. 48 | func (t *LocalTime) MarshalJSON() ([]byte, error) { 49 | if t.IsZero() { 50 | return []byte(`""`), nil 51 | } 52 | 53 | return []byte(fmt.Sprintf("%q", t.Local().Format(time.RFC3339))), nil 54 | } 55 | -------------------------------------------------------------------------------- /engine/pkg/models/observation.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | package models 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | // ObservationResult represents a result of observation session. 12 | type ObservationResult struct { 13 | Status string `json:"status"` 14 | Intervals []Interval `json:"intervals"` 15 | Summary Summary `json:"summary"` 16 | } 17 | 18 | // Interval represents data of an observation interval. 19 | type Interval struct { 20 | StartedAt time.Time `json:"started_at"` 21 | Duration float64 `json:"duration"` 22 | Warning string `json:"warning"` 23 | } 24 | 25 | // Summary represents a summary of observation. 26 | type Summary struct { 27 | TotalDuration float64 `json:"total_duration"` 28 | TotalIntervals uint `json:"total_intervals"` 29 | WarningIntervals uint `json:"warning_intervals"` 30 | Checklist Checklist `json:"checklist"` 31 | } 32 | 33 | // Checklist represents a list of observation checks. 34 | type Checklist struct { 35 | Success bool `json:"overall_success"` 36 | Duration bool `json:"session_duration_acceptable"` 37 | Locks bool `json:"no_long_dangerous_locks"` 38 | } 39 | -------------------------------------------------------------------------------- /engine/pkg/models/retrieval_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 © Postgres.ai 3 | */ 4 | 5 | package models 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestLevelByAlertType(t *testing.T) { 14 | testCases := []struct { 15 | alertType AlertType 16 | level AlertLevel 17 | }{ 18 | { 19 | alertType: "refresh_failed", 20 | level: ErrorLevel, 21 | }, 22 | { 23 | alertType: "refresh_skipped", 24 | level: WarningLevel, 25 | }, 26 | { 27 | alertType: "unknown_fail", 28 | level: UnknownLevel, 29 | }, 30 | } 31 | 32 | for _, tc := range testCases { 33 | level := AlertLevelByType(tc.alertType) 34 | assert.Equal(t, tc.level, level) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /engine/pkg/models/snapshot.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | package models 6 | 7 | // Snapshot defines a snapshot entity. 8 | type Snapshot struct { 9 | ID string `json:"id"` 10 | CreatedAt *LocalTime `json:"createdAt"` 11 | DataStateAt *LocalTime `json:"dataStateAt"` 12 | PhysicalSize uint64 `json:"physicalSize"` 13 | LogicalSize uint64 `json:"logicalSize"` 14 | Pool string `json:"pool"` 15 | NumClones int `json:"numClones"` 16 | } 17 | 18 | // SnapshotView represents a view of snapshot. 19 | type SnapshotView struct { 20 | *Snapshot 21 | PhysicalSize Size `json:"physicalSize"` 22 | LogicalSize Size `json:"logicalSize"` 23 | } 24 | -------------------------------------------------------------------------------- /engine/pkg/models/sync.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Sync defines the status of synchronization containers 4 | type Sync struct { 5 | Status Status `json:"status"` 6 | StartedAt string `json:"startedAt,omitempty"` 7 | LastReplayedLsn string `json:"lastReplayedLsn"` 8 | LastReplayedLsnAt string `json:"lastReplayedLsnAt"` 9 | ReplicationLag int `json:"replicationLag"` 10 | ReplicationUptime int `json:"replicationUptime"` 11 | } 12 | -------------------------------------------------------------------------------- /engine/pkg/util/backup/backup.go: -------------------------------------------------------------------------------- 1 | // Package backup utilities to back up and restore data 2 | package backup 3 | 4 | import ( 5 | "time" 6 | ) 7 | 8 | const ( 9 | backupFileExtension = ".bak" 10 | ) 11 | 12 | type backup struct { 13 | Filename string 14 | Time time.Time 15 | } 16 | 17 | var now = func() time.Time { 18 | return time.Now().In(time.UTC) 19 | } 20 | -------------------------------------------------------------------------------- /engine/pkg/util/backup/utils.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | "time" 8 | 9 | "github.com/pkg/errors" 10 | 11 | "gitlab.com/postgres-ai/database-lab/v3/pkg/util" 12 | ) 13 | 14 | // getFileTimestamp returns the timestamp of a backup file. 15 | // expected filename format: ...bak 16 | func getFileTimestamp(filename string) (time.Time, error) { 17 | base := filepath.Base(filename) 18 | split := strings.Split(base, ".") 19 | 20 | const expectedSize = 3 21 | 22 | if len(split) < expectedSize { 23 | return time.Time{}, fmt.Errorf("invalid filename format: %s", filename) 24 | } 25 | 26 | timeStr := split[len(split)-2] 27 | timeStamp, err := time.Parse(util.DataStateAtFormat, timeStr) 28 | 29 | if err != nil { 30 | return time.Time{}, errors.Wrap(err, "failed to parse timestamp") 31 | } 32 | 33 | return timeStamp, nil 34 | } 35 | -------------------------------------------------------------------------------- /engine/pkg/util/bytes.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | // Package util provides utility functions. Data size related functions. 6 | package util 7 | 8 | import ( 9 | "crypto/sha1" 10 | "encoding/hex" 11 | "strconv" 12 | ) 13 | 14 | // ParseBytes returns number of bytes from string. 15 | func ParseBytes(str string) (uint64, error) { 16 | return strconv.ParseUint(str, 10, 64) 17 | } 18 | 19 | // HashID returns a hash of provided string. 20 | func HashID(id string) string { 21 | h := sha1.New() 22 | _, _ = h.Write([]byte(id)) 23 | 24 | return hex.EncodeToString(h.Sum(nil)) 25 | } 26 | -------------------------------------------------------------------------------- /engine/pkg/util/clones.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2020 © Postgres.ai 3 | */ 4 | 5 | package util 6 | 7 | import ( 8 | "strconv" 9 | ) 10 | 11 | const ( 12 | // ClonePrefix defines a Database Lab clone prefix. 13 | ClonePrefix = "dblab_clone_" 14 | ) 15 | 16 | // GetCloneName returns a clone name. 17 | func GetCloneName(port uint) string { 18 | return ClonePrefix + strconv.FormatUint(uint64(port), 10) 19 | } 20 | 21 | // GetCloneNameStr returns a clone name. 22 | func GetCloneNameStr(port string) string { 23 | return ClonePrefix + port 24 | } 25 | -------------------------------------------------------------------------------- /engine/pkg/util/engine/engine.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 © Postgres.ai 3 | */ 4 | 5 | // Package engine contains tools. 6 | package engine 7 | 8 | // DefaultListenerHost defines the default host of an HTTP listener. 9 | const DefaultListenerHost = "0.0.0.0" 10 | 11 | // HTTPScheme defines the default HTTP scheme. 12 | const HTTPScheme = "http" 13 | 14 | // Localhost defines the address of the localhost host. 15 | const Localhost = "127.0.0.1" 16 | -------------------------------------------------------------------------------- /engine/pkg/util/networks/networks_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 © Postgres.ai 3 | */ 4 | 5 | package networks 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/docker/docker/api/types" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestInternalNetworks(t *testing.T) { 15 | t.Run("test internal network naming", func(t *testing.T) { 16 | instanceID := "testInstanceID" 17 | 18 | assert.Equal(t, "dle_network_testInstanceID", getNetworkName(instanceID)) 19 | }) 20 | } 21 | 22 | func TestIfContainerConnected(t *testing.T) { 23 | t.Run("test if container connected", func(t *testing.T) { 24 | resource := types.NetworkResource{ 25 | Containers: map[string]types.EndpointResource{ 26 | "testID": {Name: "test_server"}, 27 | }, 28 | } 29 | testCases := []struct { 30 | containerName string 31 | result bool 32 | }{ 33 | { 34 | containerName: "test_server", 35 | result: true, 36 | }, 37 | { 38 | containerName: "not_connected_server", 39 | result: false, 40 | }, 41 | } 42 | 43 | for _, tc := range testCases { 44 | assert.Equal(t, tc.result, hasContainerConnected(resource, tc.containerName)) 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /engine/pkg/util/projection/helpers.go: -------------------------------------------------------------------------------- 1 | // Package projection helps to bind struct fields to json/yaml paths. 2 | package projection 3 | 4 | import ( 5 | "gopkg.in/yaml.v3" 6 | ) 7 | 8 | // LoadYaml loads struct fields from yaml document. 9 | func LoadYaml(target interface{}, yaml *yaml.Node, options LoadOptions) error { 10 | soft, err := NewSoftYaml(yaml) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | return Load(target, soft, options) 16 | } 17 | 18 | // StoreYaml stores struct fields to yaml document. 19 | func StoreYaml(target interface{}, yaml *yaml.Node, options StoreOptions) error { 20 | soft, err := NewSoftYaml(yaml) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | return Store(target, soft, options) 26 | } 27 | 28 | // LoadJSON loads struct fields from json document. 29 | func LoadJSON(target interface{}, m map[string]interface{}, options LoadOptions) error { 30 | soft := NewSoftJSON(m) 31 | return Load(target, soft, options) 32 | } 33 | 34 | // StoreJSON stores struct fields to json document. 35 | func StoreJSON(target interface{}, m map[string]interface{}, options StoreOptions) error { 36 | soft := NewSoftJSON(m) 37 | return Store(target, soft, options) 38 | } 39 | -------------------------------------------------------------------------------- /engine/pkg/util/projection/load_json_test.go: -------------------------------------------------------------------------------- 1 | package projection 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestLoadJson(t *testing.T) { 10 | r := require.New(t) 11 | s := &testStruct{} 12 | 13 | err := LoadJSON(s, getJSONNormal(), LoadOptions{}) 14 | r.NoError(err) 15 | 16 | requireComplete(t, s) 17 | requireMissEmpty(t, s) 18 | } 19 | 20 | func TestLoadJsonNull(t *testing.T) { 21 | r := require.New(t) 22 | s := fullTestStruct() 23 | 24 | err := LoadJSON(s, getJSONNull(), LoadOptions{}) 25 | r.NoError(err) 26 | 27 | requireEmpty(t, s) 28 | } 29 | -------------------------------------------------------------------------------- /engine/pkg/util/projection/load_yaml_test.go: -------------------------------------------------------------------------------- 1 | package projection 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestLoadYaml(t *testing.T) { 10 | r := require.New(t) 11 | s := &testStruct{} 12 | node := getYamlNormal(t) 13 | 14 | err := LoadYaml(s, node, LoadOptions{}) 15 | r.NoError(err) 16 | 17 | requireMissEmpty(t, s) 18 | requireComplete(t, s) 19 | } 20 | 21 | func TestLoadYamlNull(t *testing.T) { 22 | r := require.New(t) 23 | s := fullTestStruct() 24 | node := getYamlNull(t) 25 | 26 | err := LoadYaml(s, node, LoadOptions{}) 27 | r.NoError(err) 28 | 29 | requireEmpty(t, s) 30 | } 31 | -------------------------------------------------------------------------------- /engine/pkg/util/projection/multi_group_test.go: -------------------------------------------------------------------------------- 1 | package projection 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | type structMulti struct { 11 | Yaml string `proj:"yaml.yamlValue" groups:"yaml"` 12 | JSON string `proj:"json.jsonValue" groups:"json"` 13 | } 14 | 15 | const yamlMulti = ` 16 | yaml: 17 | yamlValue: "yamlValue" 18 | ` 19 | 20 | func getJSONMulti() map[string]interface{} { 21 | return map[string]interface{}{ 22 | "json": map[string]interface{}{ 23 | "jsonValue": "jsonValue", 24 | }, 25 | } 26 | } 27 | 28 | func getYamlMulti(t *testing.T) *yaml.Node { 29 | t.Helper() 30 | node := &yaml.Node{} 31 | err := yaml.Unmarshal([]byte(yamlMulti), node) 32 | require.NoError(t, err) 33 | return node 34 | } 35 | 36 | func TestLoadJsonMulti(t *testing.T) { 37 | r := require.New(t) 38 | 39 | s := &structMulti{} 40 | err := LoadJSON(s, getJSONMulti(), LoadOptions{ 41 | Groups: []string{"json"}, 42 | }) 43 | r.NoError(err) 44 | 45 | err = LoadYaml(s, getYamlMulti(t), LoadOptions{ 46 | Groups: []string{"yaml"}, 47 | }) 48 | r.NoError(err) 49 | 50 | r.Equal("jsonValue", s.JSON) 51 | r.Equal("yamlValue", s.Yaml) 52 | } 53 | -------------------------------------------------------------------------------- /engine/pkg/util/projection/store_yaml_test.go: -------------------------------------------------------------------------------- 1 | package projection 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestStoreYaml(t *testing.T) { 10 | r := require.New(t) 11 | s := fullTestStruct() 12 | node := getYamlDiverted(t) 13 | 14 | err := StoreYaml(s, node, StoreOptions{}) 15 | r.NoError(err) 16 | 17 | requireYamlNormal(t, node) 18 | } 19 | 20 | func TestStoreYamlNull(t *testing.T) { 21 | r := require.New(t) 22 | s := &testStruct{} 23 | node := getYamlNormal(t) 24 | 25 | err := StoreYaml(s, node, StoreOptions{}) 26 | r.NoError(err) 27 | 28 | // no changes should have been made to the node 29 | requireYamlNullApplied(t, node) 30 | } 31 | -------------------------------------------------------------------------------- /engine/pkg/util/projection/types.go: -------------------------------------------------------------------------------- 1 | package projection 2 | 3 | import "gitlab.com/postgres-ai/database-lab/v3/pkg/util/ptypes" 4 | 5 | // FieldSet represents a field to be set in the accessor. 6 | type FieldSet struct { 7 | Path []string 8 | Value interface{} 9 | Type ptypes.Type 10 | CreateKey bool 11 | } 12 | 13 | // FieldGet is used to retrieve a field value from an accessor. 14 | type FieldGet struct { 15 | Path []string 16 | Type ptypes.Type 17 | } 18 | 19 | // Accessor is an interface for getting and setting values from a json / yaml / anything else 20 | type Accessor interface { 21 | Set(set FieldSet) error 22 | Get(get FieldGet) (interface{}, error) 23 | } 24 | -------------------------------------------------------------------------------- /engine/pkg/util/slices.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | // Package util provides utility functions. Slices related utils. 6 | package util 7 | 8 | // Unique returns unique values of slice. 9 | func Unique(list []string) []string { 10 | keys := make(map[string]struct{}) 11 | uqList := []string{} 12 | 13 | for _, entry := range list { 14 | if _, value := keys[entry]; !value { 15 | keys[entry] = struct{}{} 16 | 17 | uqList = append(uqList, entry) 18 | } 19 | } 20 | 21 | return uqList 22 | } 23 | 24 | // IncludesString checks if a string is included in a slice. 25 | func IncludesString(list []string, value string) bool { 26 | for _, entry := range list { 27 | if entry == value { 28 | return true 29 | } 30 | } 31 | 32 | return false 33 | } 34 | -------------------------------------------------------------------------------- /engine/pkg/util/slices_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | package util 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestUnique(t *testing.T) { 12 | if s := Unique([]string{"x", "y", "z"}); len(s) != 3 { 13 | t.FailNow() 14 | } 15 | 16 | if s := Unique([]string{"x", "y", "y"}); len(s) != 2 { 17 | t.FailNow() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /engine/pkg/util/testing.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | // Package util provides utility functions. Testing related. 6 | package util 7 | 8 | import ( 9 | "github.com/sergi/go-diff/diffmatchpatch" 10 | ) 11 | 12 | func diff(a string, b string) string { 13 | dmp := diffmatchpatch.New() 14 | diffs := dmp.DiffMain(a, b, false) 15 | 16 | return dmp.DiffPrettyText(diffs) 17 | } 18 | -------------------------------------------------------------------------------- /engine/pkg/util/yaml/custom.go: -------------------------------------------------------------------------------- 1 | // Package yaml contains utilities to work with YAML nodes 2 | package yaml 3 | 4 | import ( 5 | "strings" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | var secretKeyList = []string{"secret", "key", "token", "password"} 11 | 12 | // TraverseNode traverses node and mask sensitive keys. 13 | func TraverseNode(node *yaml.Node) { 14 | switch node.Kind { 15 | case yaml.DocumentNode: 16 | if len(node.Content) < 1 { 17 | return 18 | } 19 | 20 | TraverseNode(node.Content[0]) 21 | 22 | case yaml.MappingNode: 23 | for i := 0; i < len(node.Content); i += 2 { 24 | if node.Content[i+1].Kind == yaml.ScalarNode { 25 | if containsSecret(strings.ToLower(node.Content[i].Value)) { 26 | node.Content[i+1].Value = maskValue 27 | node.Content[i+1].Tag = "!!str" 28 | } 29 | 30 | continue 31 | } 32 | 33 | TraverseNode(node.Content[i+1]) 34 | } 35 | } 36 | } 37 | 38 | func containsSecret(key string) bool { 39 | for _, secret := range secretKeyList { 40 | if strings.Contains(key, secret) { 41 | return true 42 | } 43 | } 44 | 45 | return false 46 | } 47 | -------------------------------------------------------------------------------- /engine/pkg/util/yaml/default.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | // DefaultConfigMask return default config copy configuration 4 | func DefaultConfigMask() *Mask { 5 | sensitive := []string{ 6 | "server.verificationToken", 7 | "platform.accessToken", 8 | "retrieval.spec.logicalDump.options.source.connection.password", 9 | } 10 | 11 | return NewMask(sensitive) 12 | } 13 | -------------------------------------------------------------------------------- /engine/pkg/util/yaml/mask.go: -------------------------------------------------------------------------------- 1 | // Package yaml contains utilities to work with YAML nodes 2 | package yaml 3 | 4 | import ( 5 | "strings" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | const maskValue = "****" 11 | 12 | // Mask is a YAML masking utility 13 | type Mask struct { 14 | paths [][]string 15 | } 16 | 17 | // NewMask creates a new YAML copy configuration. 18 | func NewMask(paths []string) *Mask { 19 | c := &Mask{} 20 | 21 | for _, path := range paths { 22 | pathSplit := strings.Split(path, ".") 23 | c.paths = append(c.paths, pathSplit) 24 | } 25 | 26 | return c 27 | } 28 | 29 | // Yaml copies node values 30 | func (c *Mask) Yaml(node *yaml.Node) { 31 | for i := 0; i < len(c.paths); i++ { 32 | child, found := FindNodeAtPath(node, c.paths[i]) 33 | if !found { 34 | continue 35 | } 36 | 37 | if child.Kind != yaml.ScalarNode { 38 | continue 39 | } 40 | 41 | child.Value = maskValue 42 | child.Tag = "!!str" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /engine/pkg/util/yaml/mask_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | const yamlStr = ` 11 | root: 12 | sensitive: "fromValue" 13 | nonSensitive: 123 14 | ` 15 | 16 | func TestMask(t *testing.T) { 17 | r := require.New(t) 18 | node := &yaml.Node{} 19 | 20 | err := yaml.Unmarshal([]byte(yamlStr), node) 21 | r.NoError(err) 22 | 23 | mask := NewMask([]string{"root.sensitive"}) 24 | mask.Yaml(node) 25 | 26 | sensitive, _ := FindNodeAtPathString(node, "root.sensitive") 27 | r.NotNil(sensitive) 28 | r.Equal(maskValue, sensitive.Value) 29 | 30 | nonSensitive, _ := FindNodeAtPathString(node, "root.nonSensitive") 31 | r.NotNil(nonSensitive) 32 | r.Equal("123", nonSensitive.Value) 33 | } 34 | -------------------------------------------------------------------------------- /engine/pkg/util/yaml/path.go: -------------------------------------------------------------------------------- 1 | // Package yaml Utilities to work with YAML nodes 2 | package yaml 3 | 4 | import ( 5 | "strings" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | // FindNodeAtPathString finds the node at the given path. 11 | func FindNodeAtPathString(node *yaml.Node, path string) (*yaml.Node, bool) { 12 | return FindNodeAtPath(node, strings.Split(path, ".")) 13 | } 14 | 15 | // FindNodeAtPath finds the node at the given path. 16 | func FindNodeAtPath(node *yaml.Node, path []string) (*yaml.Node, bool) { 17 | if len(path) == 0 { 18 | return node, true 19 | } 20 | 21 | if node.Kind == yaml.DocumentNode { 22 | if len(node.Content) < 1 { 23 | return nil, false 24 | } 25 | 26 | return FindNodeAtPath(node.Content[0], path) 27 | } 28 | 29 | if node.Kind == yaml.MappingNode { 30 | for i := 0; i < len(node.Content); i += 2 { 31 | if node.Content[i].Value == path[0] { 32 | return FindNodeAtPath(node.Content[i+1], path[1:]) 33 | } 34 | } 35 | } 36 | 37 | return nil, false 38 | } 39 | -------------------------------------------------------------------------------- /engine/scripts/bash_autocomplete: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | : ${PROG:=$(basename ${BASH_SOURCE})} 4 | 5 | 6 | 7 | _cli_bash_autocomplete() { 8 | if [[ "${COMP_WORDS[0]}" != "source" ]]; then 9 | local cur opts base 10 | COMPREPLY=() 11 | cur="${COMP_WORDS[COMP_CWORD]}" 12 | if [[ "$cur" == "-"* ]]; then 13 | opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion ) 14 | else 15 | opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) 16 | fi 17 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 18 | return 0 19 | fi 20 | } 21 | 22 | complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG 23 | unset PROG 24 | -------------------------------------------------------------------------------- /engine/scripts/ci_docker_build_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | docker_file=${DOCKER_FILE:-"Dockerfile"} 6 | tags=${TAGS:-""} 7 | 8 | registry_user=${REGISTRY_USER:-"${CI_REGISTRY_USER}"} 9 | registry_password=${REGISTRY_PASSWORD:-"${CI_REGISTRY_PASSWORD}"} 10 | registry=${REGISTRY:-"${CI_REGISTRY}"} 11 | 12 | docker login --username $registry_user --password "${registry_password}" $registry 13 | 14 | tags_build="" 15 | tags_push="" 16 | 17 | IFS=',' read -ra ADDR string <&1 | logger --stderr --tag "cleanup_zfs_snapshot" 18 | -------------------------------------------------------------------------------- /engine/test/_prerequisites.ubuntu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | if [[ -x "$(command -v zfs)" ]] && [[ -x "$(command -v docker)" ]]; then 5 | echo "Docker and ZFS are installed – assume that secondary tools (such as jq or curl) are installed too." 6 | else 7 | # Install dependencies 8 | sudo apt-get update && sudo apt-get install -y \ 9 | apt-transport-https \ 10 | ca-certificates \ 11 | curl \ 12 | gnupg-agent \ 13 | software-properties-common \ 14 | jq 15 | 16 | # Install Docker 17 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 18 | 19 | sudo add-apt-repository \ 20 | "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ 21 | $(lsb_release -cs) \ 22 | stable" 23 | 24 | sudo apt-get install -y \ 25 | docker-ce \ 26 | docker-ce-cli \ 27 | containerd.io 28 | 29 | # Install ZFS 30 | sudo apt-get install -y \ 31 | zfsutils-linux 32 | 33 | # Install psql 34 | sudo apt-get install -y \ 35 | postgresql-client 36 | 37 | # Install yq 38 | if ! command -v yq &> /dev/null; then 39 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CC86BB64 && \ 40 | sudo add-apt-repository ppa:rmescandon/yq && \ 41 | sudo apt-get update && sudo apt-get install yq -y 42 | fi 43 | fi 44 | -------------------------------------------------------------------------------- /engine/test/_zfs.file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | DLE_TEST_MOUNT_DIR="/var/lib/test/dblab_mount" 5 | DLE_TEST_POOL_NAME="test_dblab_pool" 6 | ZFS_FILE="$(pwd)/zfs_file" 7 | 8 | # If previous run was interrupted without cleanup, 9 | # test_dblab_pool and $ZFS_FILE are still here. Cleanup. 10 | sudo zpool destroy test_dblab_pool || true 11 | sudo rm -f "${ZFS_FILE}" 12 | 13 | truncate --size 1GB "${ZFS_FILE}" 14 | 15 | sudo zpool create -f \ 16 | -O compression=on \ 17 | -O atime=off \ 18 | -O recordsize=128k \ 19 | -O logbias=throughput \ 20 | -m ${DLE_TEST_MOUNT_DIR}/${DLE_TEST_POOL_NAME} \ 21 | test_dblab_pool \ 22 | "${ZFS_FILE}" 23 | 24 | sudo zfs list 25 | -------------------------------------------------------------------------------- /engine/testdata/testdata.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2021 © Postgres.ai 3 | */ 4 | 5 | // Package testdata contains data for running tests. 6 | package testdata 7 | 8 | import ( 9 | "path" 10 | "runtime" 11 | ) 12 | 13 | // GetTestDataDir provides the current directory name. 14 | func GetTestDataDir() string { 15 | _, filename, _, _ := runtime.Caller(0) 16 | 17 | return path.Dir(filename) 18 | } 19 | -------------------------------------------------------------------------------- /engine/version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | // Package version provides the Database Lab version info. 6 | package version 7 | 8 | import ( 9 | "fmt" 10 | ) 11 | 12 | // ldflag variables. 13 | var ( 14 | version string 15 | buildTime string 16 | ) 17 | 18 | // GetVersion return the app version info. 19 | func GetVersion() string { 20 | return fmt.Sprintf("%s-%s", version, buildTime) 21 | } 22 | -------------------------------------------------------------------------------- /engine/version/version_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 2019 © Postgres.ai 3 | */ 4 | 5 | package version 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestGetVersion(t *testing.T) { 14 | version = "0.0.1" 15 | buildTime = "20200427-0551" 16 | 17 | assert.Equal(t, "0.0.1-20200427-0551", GetVersion()) 18 | } 19 | -------------------------------------------------------------------------------- /translations/unfinished-review-needed/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ui/.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | /.vscode/ 3 | /.idea/ 4 | /bin/ 5 | /.git/ 6 | **/build/** 7 | ui/node_modules/ 8 | ui/packages/ce/node_modules/ 9 | ui/packages/shared/node_modules/ 10 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Artifacts directories and reports 18 | node_modules/ 19 | build/ 20 | src/npm-debug.log 21 | packages/**/.eslintcache 22 | 23 | # Temporary files and dirs (vim, etc.) 24 | *.swp 25 | *.tmp 26 | tmp_* 27 | 28 | # IDEs 29 | .idea/ 30 | .vscode/ 31 | 32 | .DS_Store 33 | -------------------------------------------------------------------------------- /ui/.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /ui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /ui/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard-scss", 4 | "stylelint-prettier" 5 | ], 6 | "rules": { 7 | "selector-class-pattern": "^[a-z][a-zA-Z0-9]+$", 8 | "shorthand-property-no-redundant-values": null, 9 | "string-quotes": "single", 10 | "alpha-value-notation": "number" 11 | } 12 | } -------------------------------------------------------------------------------- /ui/packages/ce/.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | /.vscode/ 3 | /.idea/ 4 | /bin/ 5 | /.git/ 6 | **/build/** 7 | /ui/node_modules/ 8 | /ui/packages/ce/node_modules/ 9 | /ui/packages/shared/node_modules/ -------------------------------------------------------------------------------- /ui/packages/ce/.env: -------------------------------------------------------------------------------- 1 | PORT=3001 2 | # REACT_APP_API_URL_PREFIX 3 | # REACT_APP_WS_URL_PREFIX 4 | -------------------------------------------------------------------------------- /ui/packages/ce/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "react-app", 4 | "rules": { 5 | "@typescript-eslint/no-explicit-any": "error" 6 | } 7 | } -------------------------------------------------------------------------------- /ui/packages/ce/.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /ui/packages/ce/Dockerfile: -------------------------------------------------------------------------------- 1 | # Biuld phase. 2 | FROM node:16.14-alpine as build 3 | 4 | WORKDIR /app 5 | 6 | COPY ./ui/ . 7 | 8 | # RUN --mount=type=bind,id=pnpm,source=.pnpm-store,target=/app/.pnpm-store 9 | 10 | ARG API_URL_PREFIX 11 | ENV REACT_APP_API_URL_PREFIX ${API_URL_PREFIX} 12 | 13 | ARG WS_URL_PREFIX 14 | ENV REACT_APP_WS_URL_PREFIX ${WS_URL_PREFIX} 15 | 16 | RUN apk add --no-cache --update git && \ 17 | npm i -g pnpm@7.30.5; \ 18 | pnpm config set store-dir /app/.pnpm-store; \ 19 | pnpm set verify-store-integrity false; \ 20 | pnpm --filter @postgres.ai/ce i; \ 21 | pnpm --filter @postgres.ai/ce build 22 | 23 | # Run phase. 24 | FROM nginx:1.20.1-alpine as run 25 | 26 | COPY --from=build /app/packages/ce/build /srv/ce 27 | COPY ./ui/packages/ce/nginx.conf /etc/nginx/conf.d/ce.conf.template 28 | COPY ./ui/packages/ce/docker-entrypoint.sh / 29 | 30 | RUN rm -f /etc/nginx/conf.d/default.conf && chmod +x /docker-entrypoint.sh 31 | 32 | EXPOSE 2346 33 | 34 | ENTRYPOINT ["/docker-entrypoint.sh"] 35 | 36 | CMD ["nginx", "-g", "daemon off;"] 37 | -------------------------------------------------------------------------------- /ui/packages/ce/ci_docker_build_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | docker_file=${DOCKER_FILE:-""} 6 | tags=${TAGS:-""} 7 | 8 | registry_user=${REGISTRY_USER:-"${CI_REGISTRY_USER}"} 9 | registry_password=${REGISTRY_PASSWORD:-"${CI_REGISTRY_PASSWORD}"} 10 | registry=${REGISTRY:-"${CI_REGISTRY}"} 11 | 12 | echo "${registry_password}" | docker login --username $registry_user --password-stdin $registry 13 | 14 | tags_build="" 15 | tags_push="" 16 | 17 | IFS=',' read -ra ADDR string < /etc/nginx/conf.d/ce.conf 5 | 6 | exec "$@" 7 | -------------------------------------------------------------------------------- /ui/packages/ce/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postgres-ai/database-lab-engine/97b719af3ac1c500cbf2d1f9e266863fc15788ed/ui/packages/ce/public/favicon.ico -------------------------------------------------------------------------------- /ui/packages/ce/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Database Lab CE", 3 | "name": "Database Lab Community Edition", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Auth/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | 3 | import styles from './styles.module.scss' 4 | 5 | type Props = { 6 | children: React.ReactNode 7 | className?: string 8 | onSubmit?: React.FormEventHandler 9 | } 10 | 11 | export const Card = (props: Props) => { 12 | return ( 13 |
17 | {props.children} 18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Auth/Card/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | 3 | @keyframes fade { 4 | from { 5 | opacity: 0; 6 | transform: translateY(8px); 7 | } 8 | 9 | to { 10 | opacity: 1; 11 | transform: none; 12 | } 13 | } 14 | 15 | .root { 16 | padding: 24px; 17 | border-radius: 8px; 18 | box-shadow: $box-shadow-layer; 19 | animation: fade 0.5s both ease-out; 20 | } 21 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Auth/styles.module.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | flex: 1 1 100%; 6 | } 7 | 8 | .form { 9 | width: 320px; 10 | } 11 | 12 | .desc { 13 | margin-top: 12px; 14 | } 15 | 16 | .field { 17 | margin: 24px 0 0 0 !important; 18 | } 19 | 20 | .button { 21 | margin-top: 16px !important; 22 | } 23 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Instance/Clones/CreateClone/index.tsx: -------------------------------------------------------------------------------- 1 | import { CreateClone as CreateClonePage } from '@postgres.ai/shared/pages/CreateClone' 2 | 3 | import { PageContainer } from 'components/PageContainer' 4 | import { NavPath } from 'components/NavPath' 5 | import { ROUTES } from 'config/routes' 6 | import { getInstance } from 'api/instances/getInstance' 7 | import { getInstanceRetrieval } from 'api/instances/getInstanceRetrieval' 8 | import { getSnapshots } from 'api/snapshots/getSnapshots' 9 | import { createClone } from 'api/clones/createClone' 10 | import { getClone } from 'api/clones/getClone' 11 | 12 | export const CreateClone = () => { 13 | const routes = { 14 | clone: (cloneId: string) => 15 | ROUTES.INSTANCE.CLONES.CLONE.createPath(cloneId), 16 | } 17 | 18 | const api = { 19 | getSnapshots, 20 | getInstance, 21 | getInstanceRetrieval, 22 | createClone, 23 | getClone, 24 | } 25 | 26 | const elements = { 27 | breadcrumbs: ( 28 | 31 | ), 32 | } 33 | 34 | return ( 35 | 36 | 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Instance/Clones/index.tsx: -------------------------------------------------------------------------------- 1 | import { Switch, Route, Redirect } from 'react-router-dom' 2 | 3 | import { ROUTES } from 'config/routes' 4 | 5 | import { CreateClone } from './CreateClone' 6 | import { Clone } from './Clone' 7 | 8 | export const Clones = () => { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Instance/index.tsx: -------------------------------------------------------------------------------- 1 | import { Switch, Route, Redirect } from 'react-router-dom' 2 | 3 | import { ROUTES } from 'config/routes' 4 | 5 | import { Page } from './Page' 6 | import { Clones } from './Clones' 7 | 8 | export const Instance = () => { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.scss' 2 | import { StickyTopBar } from 'App/Menu/StickyTopBar' 3 | 4 | type Props = { 5 | displayStickyBanner: boolean | undefined 6 | menu: React.ReactNode 7 | children: React.ReactNode 8 | } 9 | 10 | export const Layout = (props: Props) => { 11 | return ( 12 |
13 | {props.displayStickyBanner && } 14 |
{props.menu}
15 |
16 | {props.children} 17 |
18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Layout/styles.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | flex: 0 0 100%; 3 | display: flex; 4 | min-height: 0; 5 | } 6 | 7 | .menu { 8 | flex: 0 0 auto; 9 | } 10 | 11 | .content { 12 | flex: 1 1 100%; 13 | display: flex; 14 | flex-direction: column; 15 | height: 100%; 16 | overflow: auto; 17 | } 18 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Menu/Header/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | @import '@postgres.ai/shared/styles/mixins'; 3 | 4 | .root { 5 | display: flex; 6 | flex-direction: column; 7 | 8 | @include touch-transition(padding); 9 | 10 | &:not(.collapsed) { 11 | padding-bottom: 16px; 12 | 13 | // border-bottom: 1px solid $color-gray; 14 | } 15 | } 16 | 17 | .header { 18 | display: flex; 19 | justify-content: flex-start; 20 | height: 32px; 21 | color: inherit; 22 | text-decoration: none; 23 | 24 | &.collapsed { 25 | justify-content: center; 26 | } 27 | } 28 | 29 | .logo { 30 | width: 36px; 31 | } 32 | 33 | .title { 34 | font-weight: 700; 35 | font-size: inherit; 36 | margin-left: 12px; 37 | white-space: nowrap; 38 | } 39 | 40 | .name { 41 | font-size: 14px; 42 | font-weight: 400; 43 | } 44 | 45 | .upgradeBtn { 46 | color: $color-orange; 47 | height: 24px; 48 | border: 1px solid $color-orange; 49 | justify-content: center; 50 | font-weight: 700; 51 | background-color: transparent; 52 | margin-top: 12px; 53 | 54 | &:hover { 55 | background-color: $color-orange; 56 | color: $color-white; 57 | } 58 | } 59 | 60 | .upgradeBtnIcon { 61 | height: 16px; 62 | width: 16px; 63 | margin-right: 6px; 64 | } 65 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Menu/Instances/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Menu/Instances/index.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite' 2 | 3 | import { ROUTES } from 'config/routes' 4 | 5 | import { ReactComponent as PlusIcon } from './icons/plus.svg' 6 | import { Button } from '@postgres.ai/shared/components/MenuButton' 7 | 8 | import styles from './styles.module.scss' 9 | 10 | type Props = { 11 | isCollapsed: boolean 12 | } 13 | 14 | export const Instances = observer((props: Props) => { 15 | return ( 16 |
17 | 27 | 37 |
38 | ) 39 | }) 40 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Menu/Instances/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | @import '@postgres.ai/shared/styles/mixins'; 3 | 4 | .root { 5 | display: flex; 6 | flex-direction: column; 7 | flex: 1 1 auto; 8 | margin: 8px 0 48px 0; 9 | min-height: 0; 10 | } 11 | 12 | .links { 13 | flex: 0 1 auto; 14 | overflow: auto; 15 | } 16 | 17 | .link, 18 | .addInstanceBtn { 19 | border: 1px solid transparent; 20 | text-decoration: none; 21 | 22 | &.selected { 23 | border-color: $color-white; 24 | } 25 | } 26 | 27 | .link { 28 | justify-content: center; 29 | white-space: normal; 30 | } 31 | 32 | .addInstanceBtn { 33 | color: $color-gray; 34 | margin-top: 8px; 35 | flex: 0 0 auto; 36 | 37 | &:hover { 38 | color: $color-white; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Menu/SignOutModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from '@postgres.ai/shared/components/Modal' 2 | import { Text } from '@postgres.ai/shared/components/Text' 3 | import { SimpleModalControls } from '@postgres.ai/shared/components/SimpleModalControls' 4 | 5 | export const SignOutModal = ({ 6 | handleSignOut, 7 | onClose, 8 | isOpen, 9 | }: { 10 | handleSignOut: () => void 11 | onClose: () => void 12 | isOpen: boolean 13 | }) => { 14 | return ( 15 | 16 | 17 | Are you sure you want to sign out? You will be redirected to the login 18 | page. 19 | 20 | 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Menu/StickyTopBar/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | @import '@postgres.ai/shared/styles/mixins'; 3 | 4 | .container { 5 | background-color: #fff2e5; 6 | position: fixed; 7 | top: 0; 8 | width: 100%; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | padding: 4px; 13 | font-size: 12px; 14 | z-index: 1000; 15 | flex-wrap: wrap; 16 | } 17 | 18 | .activateBtn { 19 | height: 19px; 20 | color: #fff; 21 | max-width: max-content; 22 | background-color: $color-orange; 23 | 24 | &:hover { 25 | color: #fff; 26 | background-color: $color-orange--hover; 27 | } 28 | 29 | &:disabled { 30 | cursor: not-allowed; 31 | background-color: #ccc; 32 | } 33 | } 34 | 35 | .spinner { 36 | margin-left: 8px; 37 | color: #fff; 38 | width: 12px !important; 39 | height: 12px !important; 40 | } 41 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Menu/StickyTopBar/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const capitalizeFirstLetter = (string: string) => { 2 | if (!string) return '' 3 | 4 | return string.charAt(0).toUpperCase() + string.slice(1) + '.' 5 | } 6 | -------------------------------------------------------------------------------- /ui/packages/ce/src/App/Menu/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | @import '@postgres.ai/shared/styles/mixins'; 3 | 4 | .root { 5 | display: flex; 6 | flex-direction: column; 7 | padding: 28px 8px 8px 8px; 8 | justify-content: space-between; 9 | background: $color-gray-dark; 10 | height: 100%; 11 | width: 192px; 12 | color: $color-white; 13 | min-height: 0; 14 | 15 | @include touch-transition(width); 16 | 17 | @media screen and (max-width: '600px') { 18 | width: 100vw; 19 | } 20 | 21 | &.collapsed { 22 | width: 64px; 23 | } 24 | } 25 | 26 | .content { 27 | display: flex; 28 | flex-direction: column; 29 | flex: 1 1 auto; 30 | min-height: 0; 31 | } 32 | 33 | .footer { 34 | display: flex; 35 | flex-direction: column; 36 | } 37 | 38 | .supportBtn { 39 | background-color: $color-orange; 40 | 41 | &:hover { 42 | background-color: $color-orange--hover; 43 | } 44 | } 45 | 46 | .collapseBtn { 47 | background: transparent; 48 | font-weight: 400; 49 | 50 | &:hover { 51 | background: $color-gray-darkest; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/clones/createClone.ts: -------------------------------------------------------------------------------- 1 | import { CreateClone } from '@postgres.ai/shared/types/api/endpoints/createClone' 2 | import { 3 | CloneDto, 4 | formatCloneDto, 5 | } from '@postgres.ai/shared/types/api/entities/clone' 6 | 7 | import { request } from 'helpers/request' 8 | 9 | export const createClone: CreateClone = async (req) => { 10 | const response = await request('/clone', { 11 | method: 'POST', 12 | body: JSON.stringify({ 13 | id: req.cloneId, 14 | snapshot: { 15 | id: req.snapshotId, 16 | }, 17 | protected: req.isProtected, 18 | db: { 19 | username: req.dbUser, 20 | password: req.dbPassword, 21 | }, 22 | }), 23 | }) 24 | 25 | return { 26 | response: response.ok 27 | ? formatCloneDto((await response.json()) as CloneDto) 28 | : null, 29 | error: response.ok ? null : response, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/clones/destroyClone.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { DestroyClone } from '@postgres.ai/shared/types/api/endpoints/destroyClone' 9 | 10 | import { request } from 'helpers/request' 11 | 12 | export const destroyClone: DestroyClone = async (req) => { 13 | const response = await request(`/clone/${req.cloneId}`, { 14 | method: 'DELETE', 15 | }) 16 | 17 | return { 18 | response: response.ok ? true : null, 19 | error: response.ok ? null : response, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/clones/getClone.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { GetClone } from '@postgres.ai/shared/types/api/endpoints/getClone' 9 | import { 10 | CloneDto, 11 | formatCloneDto, 12 | } from '@postgres.ai/shared/types/api/entities/clone' 13 | 14 | import { request } from 'helpers/request' 15 | 16 | export const getClone: GetClone = async (req) => { 17 | const response = await request(`/clone/${req.cloneId}`) 18 | 19 | return { 20 | response: response.ok 21 | ? formatCloneDto((await response.json()) as CloneDto) 22 | : null, 23 | error: response.ok ? null : response, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/clones/resetClone.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { ResetClone } from '@postgres.ai/shared/types/api/endpoints/resetClone' 9 | 10 | import { request } from 'helpers/request' 11 | 12 | export const resetClone: ResetClone = async (req) => { 13 | const response = await request(`/clone/${req.cloneId}/reset`, { 14 | method: 'POST', 15 | body: JSON.stringify({ 16 | snapshotID: req.snapshotId, 17 | }), 18 | }) 19 | 20 | return { 21 | response: response.ok ? true : null, 22 | error: response.ok ? null : response, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/clones/updateClone.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { UpdateClone } from '@postgres.ai/shared/types/api/endpoints/updateClone' 9 | 10 | import { request } from 'helpers/request' 11 | 12 | export const updateClone: UpdateClone = async (req) => { 13 | const response = await request(`/clone/${req.cloneId}`, { 14 | method: 'PATCH', 15 | body: JSON.stringify({ 16 | protected: req.clone.isProtected, 17 | }), 18 | }) 19 | 20 | return { 21 | response: response.ok ? true : null, 22 | error: response.ok ? null : response, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/configs/activateBilling.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'helpers/request' 2 | 3 | export const activateBilling = async () => { 4 | const response = await request('/admin/activate', { 5 | method: 'POST', 6 | }) 7 | 8 | return { 9 | response: response.ok ? await response.json() : null, 10 | error: response.ok ? null : await response.json(), 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/configs/getBillingStatus.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'helpers/request' 2 | 3 | export type ResponseType = { 4 | result: string 5 | billing_active: boolean 6 | recognized_org: { 7 | id: string 8 | name: string 9 | alias: string 10 | billing_page: string 11 | priveleged_until: Date 12 | } 13 | } 14 | 15 | export const getBillingStatus = async () => { 16 | const response = await request('/admin/billing-status') 17 | 18 | return { 19 | response: response.ok ? ((await response.json()) as ResponseType) : null, 20 | error: response.ok ? null : await response.json(), 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/configs/getConfig.ts: -------------------------------------------------------------------------------- 1 | import { formatConfig } from '@postgres.ai/shared/types/api/entities/config' 2 | import { request } from 'helpers/request' 3 | 4 | export const getConfig = async () => { 5 | const response = await request('/admin/config') 6 | 7 | return { 8 | response: response.ok ? formatConfig(await response.json()) : null, 9 | error: response.ok ? null : response, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/configs/getFullConfig.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'helpers/request' 2 | export const getFullConfig = async () => { 3 | const response = await request('/admin/config.yaml') 4 | .then((res) => res.blob()) 5 | .then((blob) => blob.text()) 6 | .then((yamlAsString) => { 7 | return yamlAsString 8 | }) 9 | 10 | return { 11 | response: response ? response : null, 12 | error: response && null, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/configs/getSeImages.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'helpers/request' 2 | 3 | export const getSeImages = async ({ 4 | packageGroup, 5 | platformUrl, 6 | }: { 7 | packageGroup: string 8 | platformUrl?: string 9 | }) => { 10 | const response = await request( 11 | `/dblab_se_images?package_group=eq.${packageGroup} 12 | `, 13 | {}, 14 | platformUrl, 15 | ) 16 | 17 | return { 18 | response: response.ok ? await response.json() : null, 19 | error: response.ok ? null : response, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/configs/testDbSource.ts: -------------------------------------------------------------------------------- 1 | import { dbSource } from '@postgres.ai/shared/types/api/entities/dbSource' 2 | import { request } from 'helpers/request' 3 | 4 | export const testDbSource = async (req: dbSource) => { 5 | const response = await request('/admin/test-db-source', { 6 | method: 'POST', 7 | body: JSON.stringify({ 8 | host: req.host, 9 | port: req.port.toString(), 10 | dbname: req.dbname, 11 | username: req.username, 12 | password: req.password, 13 | db_list: req.db_list 14 | }), 15 | }) 16 | 17 | return { 18 | response: response.ok ? await response.json(): null, 19 | error: response.ok ? null : await response.json() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/engine/getEngine.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EngineDto, 3 | formatEngineDto, 4 | } from '@postgres.ai/shared/types/api/endpoints/getEngine' 5 | import { request } from 'helpers/request' 6 | 7 | export const getEngine = async () => { 8 | const response = await request('/healthz') 9 | 10 | return { 11 | response: response.ok 12 | ? formatEngineDto((await response.json()) as EngineDto) 13 | : null, 14 | error: response.ok ? null : response, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/engine/getWSToken.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'helpers/request' 2 | import { formatWSTokenDto, WSTokenDTO } from '@postgres.ai/shared/types/api/entities/wsToken' 3 | import { GetWSToken } from "@postgres.ai/shared/types/api/endpoints/getWSToken"; 4 | 5 | export const getWSToken: GetWSToken = async (req ) => { 6 | const response = await request('/admin/ws-auth') 7 | 8 | return { 9 | response: response.ok 10 | ? formatWSTokenDto((await response.json()) as WSTokenDTO) 11 | : null, 12 | error: response.ok ? null : response, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/engine/initWS.ts: -------------------------------------------------------------------------------- 1 | import { InitWS } from "@postgres.ai/shared/types/api/endpoints/initWS"; 2 | import { WS_URL_PREFIX } from 'config/env' 3 | 4 | export const initWS: InitWS = (path: string, token: string): WebSocket => { 5 | let url = new URL(WS_URL_PREFIX + path, window.location.href); 6 | url.protocol = url.protocol.replace('http', 'ws'); 7 | const wsAddr = url.href + '?token=' + token; 8 | 9 | return new WebSocket(wsAddr) 10 | } 11 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/instances/getInstance.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { GetInstance } from '@postgres.ai/shared/types/api/endpoints/getInstance' 9 | import { formatInstanceDto, InstanceDto } from '@postgres.ai/shared/types/api/entities/instance' 10 | import { InstanceStateDto } from '@postgres.ai/shared/types/api/entities/instanceState' 11 | 12 | import { request } from 'helpers/request' 13 | 14 | export const getInstance: GetInstance = async () => { 15 | const response = await request('/status') 16 | 17 | // Hack to get capability with platform API. 18 | const responseDto = response.ok 19 | ? { 20 | // Fake id which means nothing. 21 | id: 0, 22 | state: (await response.json()) as InstanceStateDto, 23 | } 24 | : null 25 | 26 | return { 27 | response: responseDto ? formatInstanceDto(responseDto as InstanceDto) : null, 28 | error: response.ok ? null : response, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/instances/getInstanceRetrieval.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2022, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { request } from 'helpers/request' 9 | import { formatInstanceRetrieval } from '@postgres.ai/shared/types/api/entities/instanceRetrieval' 10 | 11 | export const getInstanceRetrieval = async () => { 12 | const response = await request('/instance/retrieval') 13 | 14 | return { 15 | response: response.ok ? formatInstanceRetrieval(await response.json()) : null, 16 | error: response.ok ? null : response, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/packages/ce/src/api/snapshots/getSnapshots.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | import { GetSnapshots } from '@postgres.ai/shared/types/api/endpoints/getSnapshots' 8 | import { 9 | SnapshotDto, 10 | formatSnapshotDto, 11 | } from '@postgres.ai/shared/types/api/entities/snapshot' 12 | 13 | import { request } from 'helpers/request' 14 | 15 | export const getSnapshots: GetSnapshots = async (req) => { 16 | const response = await request('/snapshots') 17 | 18 | return { 19 | response: response.ok 20 | ? ((await response.json()) as SnapshotDto[]).map(formatSnapshotDto) 21 | : null, 22 | error: response.ok ? null : response, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/packages/ce/src/components/NavPath/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NavLink } from 'react-router-dom' 3 | import cn from 'classnames' 4 | 5 | import styles from './styles.module.scss' 6 | 7 | export type NavRoute = { 8 | name: string 9 | path: string 10 | } 11 | 12 | type Props = { 13 | routes: NavRoute[] 14 | className?: string 15 | } 16 | 17 | export const NavPath = (props: Props) => { 18 | return ( 19 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /ui/packages/ce/src/components/NavPath/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | @import '@postgres.ai/shared/styles/mixins'; 3 | 4 | .root { 5 | display: flex; 6 | padding-bottom: 8px; 7 | border-bottom: 1px solid #ccd7da; 8 | color: $color-gray-semi-dark; 9 | font-size: 12px; 10 | font-weight: 500; 11 | } 12 | 13 | .link { 14 | color: inherit; 15 | text-decoration: none; 16 | 17 | &:hover { 18 | color: $color-blue-dark; 19 | } 20 | 21 | &.active { 22 | color: $color-black; 23 | } 24 | } 25 | 26 | .divider { 27 | margin: 0 4px; 28 | } 29 | -------------------------------------------------------------------------------- /ui/packages/ce/src/components/PageContainer/index.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | 3 | import styles from './styles.module.scss' 4 | 5 | type Props = { 6 | children: React.ReactNode 7 | className?: string 8 | } 9 | 10 | export const PageContainer = (props: Props) => { 11 | return ( 12 |
{props.children}
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /ui/packages/ce/src/components/PageContainer/styles.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | padding: 28px 24px; 3 | flex: 1 1 100%; 4 | display: flex; 5 | flex-direction: column; 6 | 7 | @media screen and (max-width: '600px') { 8 | padding: 24px 12px 24px 12px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ui/packages/ce/src/config/env.ts: -------------------------------------------------------------------------------- 1 | export const NODE_ENV = process.env.NODE_ENV 2 | export const API_URL_PREFIX = process.env.REACT_APP_API_URL_PREFIX ?? '' 3 | export const WS_URL_PREFIX = process.env.REACT_APP_WS_URL_PREFIX ?? '' 4 | export const BUILD_TIMESTAMP = process.env.BUILD_TIMESTAMP 5 | -------------------------------------------------------------------------------- /ui/packages/ce/src/config/routes.tsx: -------------------------------------------------------------------------------- 1 | export const ROUTES = { 2 | name: 'Database Lab', 3 | path: '/', 4 | 5 | AUTH: { 6 | name: 'Auth', 7 | path: '/auth', 8 | }, 9 | 10 | INSTANCE: { 11 | path: `/instance`, 12 | name: 'Instance', 13 | 14 | CLONES: { 15 | path: `/instance/clones`, 16 | 17 | CREATE: { 18 | name: 'Create clone', 19 | path: `/instance/clones/create`, 20 | }, 21 | 22 | CLONE: { 23 | name: 'Clone', 24 | createPath: (cloneId = ':cloneId') => `/instance/clones/${cloneId}`, 25 | }, 26 | }, 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /ui/packages/ce/src/helpers/edition.ts: -------------------------------------------------------------------------------- 1 | import { appStore } from "stores/app"; 2 | 3 | const communityEdition = 'Community Edition' 4 | const standardEdition = 'Standard Edition' 5 | 6 | export const DLEEdition = (): string => { 7 | switch (appStore.engine?.data?.edition) { 8 | case 'standard': 9 | return standardEdition 10 | 11 | case 'community': 12 | return communityEdition 13 | 14 | default: 15 | return communityEdition 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui/packages/ce/src/helpers/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { LocalStorage as LocalStorageShared } from '@postgres.ai/shared/helpers/localStorage' 2 | 3 | class LocalStorage extends LocalStorageShared {} 4 | 5 | export const localStorage = new LocalStorage() 6 | -------------------------------------------------------------------------------- /ui/packages/ce/src/helpers/request.ts: -------------------------------------------------------------------------------- 1 | import { 2 | request as requestCore, 3 | RequestOptions, 4 | } from '@postgres.ai/shared/helpers/request' 5 | 6 | import { localStorage } from 'helpers/localStorage' 7 | import { appStore } from 'stores/app' 8 | import { API_URL_PREFIX } from 'config/env' 9 | 10 | export const request = async ( 11 | path: string, 12 | options?: RequestOptions, 13 | customPrefix?: string, 14 | ) => { 15 | const authToken = localStorage.getAuthToken() 16 | 17 | const response = await requestCore( 18 | `${customPrefix ? customPrefix?.replace(/"/g, '') : API_URL_PREFIX}${path}`, 19 | { 20 | ...options, 21 | headers: { 22 | ...(authToken && { 'Verification-Token': authToken }), 23 | ...options?.headers, 24 | }, 25 | }, 26 | ) 27 | 28 | if (response.status === 401) { 29 | appStore.setIsInvalidAuthToken() 30 | localStorage.removeAuthToken() 31 | } else { 32 | appStore.setIsValidAuthToken() 33 | } 34 | 35 | return response 36 | } 37 | -------------------------------------------------------------------------------- /ui/packages/ce/src/index.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/global'; 2 | -------------------------------------------------------------------------------- /ui/packages/ce/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { ThemeProvider } from '@material-ui/core' 4 | 5 | import { theme } from '@postgres.ai/shared/styles/theme' 6 | 7 | import './index.scss' 8 | 9 | import { App } from './App' 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById('root'), 18 | ) 19 | -------------------------------------------------------------------------------- /ui/packages/ce/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // Env types. 4 | declare namespace NodeJS { 5 | interface ProcessEnv { 6 | readonly REACT_APP_API_URL_PREFIX?: string 7 | readonly REACT_APP_WS_URL_PREFIX?: string 8 | readonly BUILD_TIMESTAMP: number 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ui/packages/ce/src/stores/app.ts: -------------------------------------------------------------------------------- 1 | import { makeAutoObservable } from 'mobx' 2 | 3 | import { getEngine } from 'api/engine/getEngine' 4 | import { EngineType } from '@postgres.ai/shared/types/api/endpoints/getEngine' 5 | 6 | type EngineProp = { 7 | data: EngineType | null | undefined 8 | isLoading: boolean 9 | } 10 | 11 | class AppStore { 12 | readonly engine: EngineProp = { 13 | data: undefined, 14 | isLoading: false, 15 | } 16 | 17 | isValidAuthToken: boolean | undefined = undefined 18 | 19 | constructor() { 20 | makeAutoObservable(this) 21 | } 22 | 23 | loadData = async () => { 24 | this.engine.isLoading = true 25 | const { response } = await getEngine() 26 | this.engine.data = response 27 | this.engine.isLoading = false 28 | } 29 | 30 | setIsValidAuthToken = () => (this.isValidAuthToken = true) 31 | 32 | setIsInvalidAuthToken = () => (this.isValidAuthToken = false) 33 | } 34 | 35 | export const appStore = new AppStore() 36 | -------------------------------------------------------------------------------- /ui/packages/ce/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": "src", 19 | "noImplicitAny": true 20 | }, 21 | "include": ["src", "../shared"] 22 | } 23 | -------------------------------------------------------------------------------- /ui/packages/shared/.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /ui/packages/shared/components/AlertSnackbar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Snackbar from '@mui/material/Snackbar'; 3 | import Alert from '@mui/material/Alert'; 4 | import {useAlertSnackbar} from "./useAlertSnackbar"; 5 | 6 | export const AlertSnackbar = () => { 7 | const { snackbarMessage, closeSnackbar } = useAlertSnackbar(); 8 | return ( 9 | 13 | 19 | {snackbarMessage?.message} 20 | 21 | 22 | ) 23 | } -------------------------------------------------------------------------------- /ui/packages/shared/components/Button2/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cn from 'classnames' 3 | 4 | import { Spinner } from '@postgres.ai/shared/components/Spinner' 5 | 6 | import styles from './styles.module.scss' 7 | 8 | type Props = { 9 | type?: React.ButtonHTMLAttributes['type'] 10 | children: React.ReactNode 11 | onClick?: React.DOMAttributes['onClick'] 12 | size?: 'sm' | 'md' | 'lg' 13 | theme?: 'primary' | 'secondary' | 'accent' 14 | className?: string 15 | isDisabled?: boolean 16 | isLoading?: boolean 17 | } 18 | 19 | export const Button = React.forwardRef( 20 | (props, ref) => { 21 | const { type = 'button', size = 'md', theme = 'secondary' } = props 22 | 23 | const isDisabled = props.isDisabled || props.isLoading 24 | 25 | return ( 26 | 41 | ) 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /ui/packages/shared/components/FormattedText/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | @import '@postgres.ai/shared/styles/mixins'; 3 | 4 | .root { 5 | position: relative; 6 | background: $color-white--100; 7 | border: 1px solid $color-gray-100; 8 | border-radius: $border-radius--small; 9 | overflow: hidden; 10 | 11 | &:hover .copyButtonContainer { 12 | opacity: 1; 13 | } 14 | } 15 | 16 | .content { 17 | padding: 10px 80px 10px 10px; 18 | line-height: 1.5; 19 | overflow: auto; 20 | max-height: inherit; 21 | margin: 0; 22 | } 23 | 24 | .copyButtonContainer { 25 | opacity: 0; 26 | 27 | @include touch-transition(opacity); 28 | } 29 | 30 | .copyButton { 31 | position: absolute; 32 | top: 8.5px; 33 | right: 8.5px; 34 | } 35 | -------------------------------------------------------------------------------- /ui/packages/shared/components/GatewayLink/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import cn from 'classnames' 9 | 10 | import styles from '@postgres.ai/shared/components/Link2/styles.module.scss' 11 | 12 | type Props = React.DetailedHTMLProps< 13 | React.AnchorHTMLAttributes, 14 | HTMLAnchorElement 15 | > 16 | 17 | export const GatewayLink = (props: Props) => { 18 | const { 19 | rel = 'noopener noreferrer', 20 | target = '_blank', 21 | className, 22 | ...otherProps 23 | } = props 24 | 25 | return ( 26 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /ui/packages/shared/components/HorizontalScrollContainer/types.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | export type Dimensions = { 9 | offsetWidth: number; 10 | scrollWidth: number; 11 | scrollLeft: number; 12 | }; 13 | -------------------------------------------------------------------------------- /ui/packages/shared/components/HorizontalScrollContainer/utils.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { Dimensions } from './types'; 9 | 10 | export const checkIsVisibleLeftCurtain = (dimensions: Dimensions) => { 11 | return dimensions.scrollLeft > 0; 12 | }; 13 | 14 | export const checkIsVisibleRightCurtain = (dimensions: Dimensions) => { 15 | return dimensions.scrollLeft < dimensions.scrollWidth - dimensions.offsetWidth; 16 | }; 17 | -------------------------------------------------------------------------------- /ui/packages/shared/components/ImportantText/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import React from 'react' 9 | import { makeStyles } from '@material-ui/core' 10 | 11 | type Props = { 12 | children: React.ReactNode 13 | } 14 | 15 | const useStyles = makeStyles( 16 | { 17 | root: { 18 | fontWeight: 'bold', 19 | whiteSpace: 'nowrap', 20 | }, 21 | }, 22 | { index: 1 }, 23 | ) 24 | 25 | export const ImportantText = (props: Props) => { 26 | const classes = useStyles() 27 | 28 | return {props.children} 29 | } 30 | -------------------------------------------------------------------------------- /ui/packages/shared/components/Link2/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { Link as LinkBase, LinkProps } from 'react-router-dom' 9 | import cn from 'classnames' 10 | 11 | import styles from './styles.module.scss' 12 | 13 | type Props = LinkProps & { external?: boolean } 14 | 15 | export const Link = (props: Props) => { 16 | const { className, external, to, ...otherProps } = props 17 | 18 | if (external) { 19 | return ( 20 | 27 | ) 28 | } 29 | 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /ui/packages/shared/components/Link2/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | @import '@postgres.ai/shared/styles/mixins'; 3 | 4 | .root { 5 | color: $color-blue-main; 6 | 7 | @include touch-transition(color); 8 | 9 | &:hover { 10 | color: $color-blue-dark; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ui/packages/shared/components/MenuButton/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | @import '@postgres.ai/shared/styles/mixins'; 3 | 4 | .root { 5 | display: flex; 6 | align-items: center; 7 | padding: 0 10px; 8 | background-color: $color-gray-darkest; 9 | cursor: pointer; 10 | border-radius: 4px; 11 | border: 0; 12 | height: 48px; 13 | width: 100%; 14 | color: inherit; 15 | white-space: nowrap; 16 | overflow: hidden; 17 | font-weight: 700; 18 | text-decoration: none; 19 | 20 | @include touch-transition(background-color color); 21 | 22 | &:hover { 23 | background-color: $color-gray-darkest--hover; 24 | color: inherit; 25 | } 26 | 27 | &.collapsed { 28 | justify-content: center; 29 | } 30 | 31 | + .root { 32 | margin-top: 8px; 33 | } 34 | } 35 | 36 | .icon { 37 | font-size: 0; 38 | 39 | &:not(.collapsed) { 40 | margin-right: 8px; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/packages/shared/components/PageSpinner/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { Spinner } from '@postgres.ai/shared/components/Spinner' 9 | 10 | import styles from './styles.module.scss' 11 | 12 | export const PageSpinner = () => { 13 | return ( 14 |
15 | 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /ui/packages/shared/components/PageSpinner/styles.module.scss: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | .root { 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | flex: 1 1 100%; 13 | } 14 | -------------------------------------------------------------------------------- /ui/packages/shared/components/Spinner/icon.tsx: -------------------------------------------------------------------------------- 1 | export const SpinnerIcon = ({ className }: { className: string }) => ( 2 | 11 | Oval 12 | 21 | 27 | 28 | 29 | ) 30 | -------------------------------------------------------------------------------- /ui/packages/shared/components/Spinner/index.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | import { SpinnerIcon } from './icon' 3 | 4 | import styles from './styles.module.scss' 5 | 6 | export type Size = 'sm' | 'md' | 'lg' 7 | 8 | export type Props = { 9 | size?: Size 10 | className?: string 11 | } 12 | 13 | export const Spinner = (props: Props) => { 14 | const { size = 'md' } = props 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /ui/packages/shared/components/Spinner/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | 3 | @keyframes rotation { 4 | from { 5 | transform: rotate(0); 6 | } 7 | 8 | to { 9 | transform: rotate(360deg); 10 | } 11 | } 12 | 13 | .root { 14 | color: $color-orange; 15 | 16 | animation: rotation ease-in-out .5s infinite; 17 | 18 | // Sizes. 19 | &.sm { 20 | height: 16px; 21 | width: 16px; 22 | } 23 | 24 | &.md { 25 | height: 24px; 26 | width: 24px; 27 | } 28 | 29 | &.lg { 30 | height: 32px; 31 | width: 32px; 32 | } 33 | } -------------------------------------------------------------------------------- /ui/packages/shared/components/Status/styles.module.scss: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | @import '@postgres.ai/shared/styles/vars'; 9 | 10 | .root { 11 | &.ok { 12 | color: $color-status-ok; 13 | } 14 | 15 | &.warning { 16 | color: $color-status-warning; 17 | } 18 | 19 | &.error { 20 | color: $color-status-error; 21 | } 22 | 23 | &.waiting { 24 | color: $color-status-waiting; 25 | } 26 | 27 | &.unknown { 28 | color: $color-status-unknown; 29 | } 30 | } 31 | 32 | .iconContainer { 33 | display: inline-block; 34 | margin-right: 0.5em; 35 | width: 0.8em; 36 | position: relative; 37 | } 38 | 39 | .icon { 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | width: 100%; 44 | height: 100%; 45 | } 46 | -------------------------------------------------------------------------------- /ui/packages/shared/components/StubContainer/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import React from 'react' 9 | import { makeStyles } from '@material-ui/core' 10 | import Box from '@mui/material/Box' 11 | import clsx from 'clsx' 12 | 13 | const useStyles = makeStyles( 14 | { 15 | root: { 16 | padding: '20px 0', 17 | flex: '1 1 100%', 18 | }, 19 | }, 20 | { index: 1 }, 21 | ) 22 | 23 | type Props = { 24 | className?: string 25 | children: React.ReactNode 26 | } 27 | 28 | export const StubContainer = (props: Props) => { 29 | const classes = useStyles() 30 | 31 | return ( 32 | 38 | {props.children} 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /ui/packages/shared/components/StubSpinnerFlex/index.tsx: -------------------------------------------------------------------------------- 1 | import cn from 'classnames' 2 | 3 | import { Spinner, Size } from '@postgres.ai/shared/components/Spinner' 4 | 5 | import styles from './styles.module.scss' 6 | 7 | type Props = { 8 | mode?: 'absolute' | 'flex' 9 | size?: Size 10 | className?: string 11 | } 12 | 13 | export const StubSpinner = (props: Props) => { 14 | const { mode = 'flex', size = 'lg' } = props 15 | return ( 16 |
17 | 18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /ui/packages/shared/components/StubSpinnerFlex/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | 3 | .root { 4 | background: $color-white; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | width: 100%; 9 | height: 100%; 10 | 11 | &.absolute { 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | } 16 | 17 | &.flex { 18 | flex: 1 1 100%; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/packages/shared/components/Text/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import React from 'react' 9 | import { makeStyles } from '@material-ui/core' 10 | 11 | type Props = { 12 | children: React.ReactNode 13 | } 14 | 15 | const useStyles = makeStyles( 16 | { 17 | root: { 18 | margin: 0, 19 | }, 20 | }, 21 | { index: 1 }, 22 | ) 23 | 24 | export const Text = (props: Props) => { 25 | const classes = useStyles() 26 | 27 | return

{props.children}

28 | } 29 | -------------------------------------------------------------------------------- /ui/packages/shared/config/index.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from 'date-fns' 2 | import { enUS } from 'date-fns/locale' 3 | import { getUserLocale } from 'get-user-locale' 4 | 5 | type Config = { 6 | dateFnsLocale: Locale 7 | appName: string 8 | } 9 | 10 | export const config: Config = { 11 | dateFnsLocale: enUS, 12 | appName: 'Postgres.ai', 13 | } 14 | 15 | const loadDateFnsLocale = async () => { 16 | const userLocale = getUserLocale() 17 | 18 | // We are already using this locale. 19 | if (userLocale === config.dateFnsLocale.code) return 20 | 21 | try { 22 | const locale = await import(`date-fns/locale/${userLocale}`) 23 | config.dateFnsLocale = locale.default 24 | return 25 | } catch (e) { 26 | // Unavailable locale. 27 | } 28 | } 29 | 30 | export const initConfig = async () => { 31 | await loadDateFnsLocale() 32 | } 33 | -------------------------------------------------------------------------------- /ui/packages/shared/config/links.ts: -------------------------------------------------------------------------------- 1 | export const linksConfig = { 2 | cloudSignIn: 'https://postgres.ai/pricing', 3 | docs: 'https://postgres.ai/docs', 4 | support: 'https://postgres.ai/contact', 5 | github: 'https://github.com/postgres-ai/database-lab-engine', 6 | } 7 | -------------------------------------------------------------------------------- /ui/packages/shared/helpers/localStorage.ts: -------------------------------------------------------------------------------- 1 | const keys = { 2 | // Keep this name. 3 | authToken: 'token', 4 | } 5 | 6 | export class LocalStorage { 7 | // Auth token. 8 | getAuthToken = () => window.localStorage.getItem(keys.authToken) 9 | 10 | setAuthToken = (value: string) => window.localStorage.setItem(keys.authToken, value) 11 | 12 | removeAuthToken = () => window.localStorage.removeItem(keys.authToken) 13 | } 14 | 15 | export const localStorage = new LocalStorage() 16 | -------------------------------------------------------------------------------- /ui/packages/shared/helpers/request.ts: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch' 2 | 3 | type RequestParams = Record 4 | 5 | export type RequestOptions = RequestInit & { 6 | params?: RequestParams 7 | } 8 | 9 | const serializeParams = (params: RequestParams | null) => { 10 | if (!params) return null 11 | 12 | const searchParams = new URLSearchParams() 13 | 14 | Object.entries(params).map((param) => { 15 | const [key, value] = param 16 | searchParams.append(key, String(value)) 17 | }) 18 | 19 | return searchParams.toString() 20 | } 21 | 22 | const createUrl = (path: string, params: RequestParams | null) => { 23 | const serializedParams = serializeParams(params) 24 | const queryString = serializedParams ? `?${serializedParams}` : '' 25 | return `${path}${queryString}` 26 | } 27 | 28 | export const request = async (path: string, options?: RequestOptions) => { 29 | const { params = null, ...requestInit } = options ?? {} 30 | 31 | const url = createUrl(path, params) 32 | 33 | try { 34 | return await window.fetch(url, { 35 | ...requestInit, 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | ...requestInit?.headers, 39 | }, 40 | }) 41 | } catch (e) { 42 | return new Response(null, { 43 | status: 500, 44 | statusText: `Unknown error`, 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/packages/shared/hooks/useWindowDimensions.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const useWindowDimensions = () => { 4 | const [windowDimensions, setWindowDimensions] = useState(window.innerWidth) 5 | 6 | useEffect(() => { 7 | const handleResize = () => { 8 | setWindowDimensions(window.innerWidth) 9 | } 10 | 11 | window.addEventListener('resize', handleResize) 12 | return () => window.removeEventListener('resize', handleResize) 13 | }, []) 14 | 15 | return windowDimensions 16 | } 17 | -------------------------------------------------------------------------------- /ui/packages/shared/icons/ArrowDropDown/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import React from 'react' 9 | 10 | type Props = { 11 | className?: string 12 | } 13 | 14 | export const ArrowDropDownIcon = (props: Props) => { 15 | return ( 16 | 22 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /ui/packages/shared/icons/Circle/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import React from 'react' 9 | 10 | type Props = { 11 | className?: string 12 | } 13 | 14 | export const CircleIcon = (props: Props) => { 15 | const { className } = props 16 | 17 | return ( 18 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /ui/packages/shared/icons/External/index.tsx: -------------------------------------------------------------------------------- 1 | export const ExternalIcon = ({ className }: { className: string }) => ( 2 | 9 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /ui/packages/shared/icons/Info/index.tsx: -------------------------------------------------------------------------------- 1 | export const InfoIcon = ({ className }: { className?: string }) => ( 2 | 7 | 8 | 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /ui/packages/shared/icons/Shield/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import React from 'react' 9 | 10 | type Props = { 11 | className?: string 12 | } 13 | 14 | export const ShieldIcon = React.forwardRef( 15 | (props, ref) => { 16 | const { className, ...hiddenProps } = props 17 | return ( 18 | 26 | 30 | 31 | ) 32 | }, 33 | ) 34 | -------------------------------------------------------------------------------- /ui/packages/shared/meta.json: -------------------------------------------------------------------------------- 1 | {"buildDate":1634829055606} 2 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Clone/context.ts: -------------------------------------------------------------------------------- 1 | import { createStrictContext } from '@postgres.ai/shared/utils/react' 2 | 3 | import { Api } from './stores/Main' 4 | import { Stores } from './useCreatedStores' 5 | 6 | export type Host = { 7 | instanceId: string 8 | cloneId: string 9 | routes: { 10 | instance: () => string 11 | } 12 | api: Api 13 | elements: { 14 | breadcrumbs: React.ReactNode 15 | } 16 | } 17 | 18 | export const { useStrictContext: useHost, Provider: HostProvider } = 19 | createStrictContext() 20 | 21 | export const { useStrictContext: useStores, Provider: StoresProvider } = 22 | createStrictContext() 23 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Clone/useCreatedStores.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | import { MainStore } from './stores/Main' 4 | 5 | import { Host } from './context' 6 | 7 | export const useCreatedStores = (host: Host) => ({ 8 | main: useMemo(() => new MainStore(host.api), []), 9 | }) 10 | 11 | export type Stores = ReturnType 12 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/CreateClone/styles.module.scss: -------------------------------------------------------------------------------- 1 | .title { 2 | margin-top: 16px; 3 | } 4 | 5 | .form { 6 | max-width: 425px; 7 | margin-top: 8px; 8 | } 9 | 10 | .section { 11 | & + .section { 12 | margin-top: 32px; 13 | } 14 | } 15 | 16 | .title { 17 | font-size: inherit; 18 | font-weight: 700; 19 | } 20 | 21 | .text { 22 | margin-top: 8px; 23 | } 24 | 25 | .remark { 26 | font-size: 12px; 27 | } 28 | 29 | .error { 30 | color: red; 31 | } 32 | 33 | .snapshotTag { 34 | font-weight: 700; 35 | margin-left: 4px; 36 | } 37 | 38 | .summary { 39 | display: flex; 40 | align-items: center; 41 | background-color: rgba(#808080, 0.15) !important; 42 | padding: 12px; 43 | } 44 | 45 | .summaryIcon { 46 | width: 30px; 47 | color: silver; 48 | margin-right: 12px; 49 | } 50 | 51 | .params { 52 | flex: 1 1 100%; 53 | } 54 | 55 | .param { 56 | display: flex; 57 | justify-content: space-between; 58 | } 59 | 60 | .controls { 61 | display: flex; 62 | align-items: center; 63 | } 64 | 65 | .spinner { 66 | margin-left: 8px; 67 | } 68 | 69 | .elapsedTime { 70 | margin-left: 24px; 71 | } 72 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/CreateClone/useCreatedStores.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | import { MainStore, MainStoreApi } from './stores/Main' 4 | 5 | export const useCreatedStores = (api: MainStoreApi) => ({ 6 | main: useMemo(() => new MainStore(api), []), 7 | }) 8 | 9 | export type Stores = ReturnType 10 | 11 | export type { MainStoreApi } 12 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/CreateClone/useForm.ts: -------------------------------------------------------------------------------- 1 | import { useFormik } from 'formik' 2 | import * as Yup from 'yup' 3 | 4 | export type FormValues = { 5 | cloneId: string 6 | snapshotId: string 7 | dbUser: string 8 | dbPassword: string 9 | isProtected: boolean 10 | } 11 | 12 | const Schema = Yup.object().shape({ 13 | cloneId: Yup.string(), 14 | snapshotId: Yup.string().required('Date state time is required'), 15 | dbUser: Yup.string().required('Database username is required'), 16 | dbPassword: Yup.string().required('Database password is required'), 17 | isProtected: Yup.boolean(), 18 | }) 19 | 20 | export const useForm = (onSubmit: (values: FormValues) => void) => { 21 | const formik = useFormik({ 22 | initialValues: { 23 | cloneId: '', 24 | snapshotId: '', 25 | dbUser: '', 26 | dbPassword: '', 27 | isProtected: false, 28 | }, 29 | validationSchema: Schema, 30 | onSubmit, 31 | validateOnBlur: false, 32 | validateOnChange: false, 33 | }) 34 | 35 | return formik 36 | } 37 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Clones/Header/Item/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.scss' 2 | 3 | type Props = { 4 | value: React.ReactNode 5 | children: React.ReactNode 6 | } 7 | 8 | export const Item = (props: Props) => { 9 | return ( 10 |
11 |
{props.value}
12 |
{props.children}
13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Clones/Header/Item/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | 3 | .root { 4 | font-size: 12px; 5 | text-align: center; 6 | color: $color-gray-semi-dark; 7 | } 8 | 9 | .value { 10 | font-weight: bold; 11 | font-size: 14px; 12 | color: $color-black; 13 | } 14 | 15 | .description { 16 | margin-top: 4px; 17 | } 18 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Clones/Header/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/mixins'; 2 | 3 | .root { 4 | display: flex; 5 | justify-content: space-between; 6 | padding: 20px; 7 | 8 | @include sm { 9 | padding: 20px 0; 10 | } 11 | } -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/ClonesModal/utils.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | export const getTags = ({ 9 | pool, 10 | snapshotId, 11 | }: { 12 | pool: string | null 13 | snapshotId: string | null 14 | }) => { 15 | const tags = [] 16 | 17 | if (pool) tags.push({ name: 'Disk', value: pool }) 18 | if (snapshotId) tags.push({ name: 'Snapshot', value: snapshotId }) 19 | 20 | return tags 21 | } 22 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/InactiveInstance/utils.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | export const formatTimestampUtc = (timestamp: moment.MomentInput) => { 4 | if (!timestamp) { 5 | return null 6 | } 7 | 8 | return moment(timestamp).utc().format('YYYY-MM-DD HH:mm:ss UTC') 9 | } 10 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Connection/ConnectModal/Content/utils.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { Instance } from '@postgres.ai/shared/types/api/entities/instance' 9 | 10 | export const getCliInitCommand = (instance: Instance) => 11 | `dblab init --url ${instance.url} --token TOKEN --environment-id ${instance.projectName}` 12 | 13 | export const getSshPortForwardingCommand = (instance: Instance) => { 14 | if (instance.sshServerUrl) { 15 | // Parse the URL to get the port 16 | const url = new URL(instance.url as string) 17 | const port = url.port || '2345' 18 | // Here we hard-code the API port on the server (2345)- this is a requirement now 19 | // for all DBLab instances working via tunnel, per decision made (NIkolayS 2024-05-22) 20 | return `ssh -NTML ${port}:localhost:2345 ${instance.sshServerUrl} -i ~/.ssh/id_rsa` 21 | } else { 22 | return null 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Connection/ConnectModal/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { useState } from 'react' 9 | 10 | import { Button } from '@postgres.ai/shared/components/Button2' 11 | import { Modal } from '@postgres.ai/shared/components/Modal' 12 | 13 | import { Content } from './Content' 14 | 15 | type Props = { 16 | className?: string 17 | } 18 | 19 | export const ConnectModal = (props: Props) => { 20 | const [isOpen, setIsOpen] = useState(false) 21 | 22 | const handleClickOpen = () => setIsOpen(true) 23 | 24 | const handleClose = () => setIsOpen(false) 25 | 26 | return ( 27 | <> 28 | 31 | 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Details/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import React from 'react'; 9 | 10 | import { Section } from '../components/Section'; 11 | import { Property } from '../components/Property'; 12 | 13 | export const Details = () => { 14 | return ( 15 |
16 | postgres:12 17 | 0.4.4 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Disks/Disk/Marker/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { makeStyles } from '@material-ui/core' 3 | import clsx from 'clsx' 4 | 5 | import { CircleIcon } from '@postgres.ai/shared/icons/Circle' 6 | 7 | type Props = { 8 | className: string 9 | } 10 | 11 | const useStyles = makeStyles( 12 | { 13 | root: { 14 | display: 'inline', 15 | verticalAlign: 'middle', 16 | width: '10px', 17 | }, 18 | }, 19 | { index: 1 }, 20 | ) 21 | 22 | export const Marker = (props: Props) => { 23 | const classes = useStyles() 24 | 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Disks/Disk/ProgressBar/PointerIcon.tsx: -------------------------------------------------------------------------------- 1 | export const PoinerIcon = ({ 2 | className, 3 | style, 4 | }: { 5 | className?: string 6 | 7 | style?: React.CSSProperties 8 | }) => ( 9 | 18 | 19 | 20 | ) 21 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Retrieval/RefreshFailedAlert/index.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite' 2 | 3 | import { useStores } from '@postgres.ai/shared/pages/Instance/context' 4 | import { WarningIcon } from '@postgres.ai/shared/icons/Warning' 5 | import { formatDateStd } from '@postgres.ai/shared/utils/date' 6 | 7 | import { Property } from '../../components/Property' 8 | 9 | import styles from './styles.module.scss' 10 | 11 | export const RefreshFailedAlert = observer(() => { 12 | const stores = useStores() 13 | 14 | const refreshFailed = 15 | stores.main.instance?.state?.retrieving?.alerts?.refreshFailed 16 | if (!refreshFailed) return null 17 | 18 | return ( 19 |
20 |
21 |
{refreshFailed.message}
22 | 23 |
24 | 25 | {formatDateStd(refreshFailed.lastSeen, { withDistance: true })} 26 | 27 | 28 | {refreshFailed.count} 29 | 30 |
31 | ) 32 | }) 33 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Retrieval/RefreshFailedAlert/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | 3 | .root { 4 | padding: 8px 10px; 5 | background: #fff2e5; 6 | border: 1px solid $color-gray-100; 7 | margin-top: 12px; 8 | border-radius: $border-radius--small; 9 | } 10 | 11 | .header { 12 | display: flex; 13 | justify-content: space-between; 14 | align-items: center; 15 | margin-bottom: 8px; 16 | } 17 | 18 | .title { 19 | font-weight: 700; 20 | font-size: 12px; 21 | margin-right: 24px; 22 | margin: 0; 23 | } 24 | 25 | .icon { 26 | width: 10px; 27 | color: $color-status-warning; 28 | flex: 0 0 auto; 29 | } 30 | 31 | .propertyName { 32 | flex: 0 0 123px; 33 | } 34 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Retrieval/RetrievalModal/styles.module.scss: -------------------------------------------------------------------------------- 1 | .tableContainer { 2 | display: flex; 3 | width: 100%; 4 | justify-content: space-between; 5 | flex-direction: row; 6 | } -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Retrieval/RetrievalTable/styles.module.scss: -------------------------------------------------------------------------------- 1 | .tableRow { 2 | display: inline-table !important; 3 | width: 100%; 4 | } 5 | 6 | .tableBody { 7 | height: 100%; 8 | 9 | div:not(:first-child) { 10 | padding-top: 0.75rem; 11 | border-top: 1px solid rgba(224, 224, 224, 1); 12 | } 13 | 14 | div { 15 | padding: 0.75rem 0; 16 | } 17 | 18 | td { 19 | border-bottom: 0; 20 | padding: 0px 18px; 21 | font-size: 13px; 22 | font-family: 'Fira Code', monospace; 23 | } 24 | } 25 | 26 | .tableSubtitle { 27 | font-size: 15px; 28 | font-weight: bold; 29 | } -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Retrieval/utils.ts: -------------------------------------------------------------------------------- 1 | export const getTypeByStatus = (status: string | undefined) => { 2 | if (status === 'finished') return 'ok' 3 | if (status === 'refreshing') return 'waiting' 4 | if (status === 'failed') return 'error' 5 | return 'unknown' 6 | } 7 | 8 | export const isRetrievalUnknown = (mode: string | undefined) => { 9 | return mode === 'unknown' || mode === '' 10 | } 11 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Snapshots/utils.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { Snapshot } from '@postgres.ai/shared/types/api/entities/snapshot' 9 | 10 | export const getEdgeSnapshots = (snapshots: Snapshot[]) => { 11 | const list = [...snapshots] 12 | const [first] = list 13 | const [last] = list.reverse() 14 | return { 15 | firstSnapshot: (first as Snapshot | undefined) ?? null, 16 | lastSnapshot: (last as Snapshot | undefined) ?? null 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Status/InstanceResponseModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite' 2 | 3 | import { Modal } from '@postgres.ai/shared/components/Modal' 4 | import { FormattedText } from '@postgres.ai/shared/components/FormattedText' 5 | import { useStores } from '@postgres.ai/shared/pages/Instance/context' 6 | 7 | import styles from './styles.module.scss' 8 | 9 | type Props = { 10 | isOpen: boolean 11 | onClose: () => void 12 | } 13 | 14 | export const InstanceResponseModal = observer((props: Props) => { 15 | const stores = useStores() 16 | 17 | if (!stores.main.instance) return null 18 | 19 | return ( 20 | 26 | 30 | 31 | ) 32 | }) 33 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Status/InstanceResponseModal/styles.module.scss: -------------------------------------------------------------------------------- 1 | .formattedText { 2 | max-height: 400px; 3 | } 4 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Status/styles.module.scss: -------------------------------------------------------------------------------- 1 | .controls { 2 | display: flex; 3 | margin-top: 24px; 4 | } 5 | 6 | .button { 7 | text-decoration: none; 8 | 9 | & + .button { 10 | margin-left: 8px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/Status/utils.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { capitalize } from '@postgres.ai/shared/utils/strings' 9 | 10 | const STATUS_CODE_TO_TYPE = { 11 | OK: 'ok' as const, 12 | WARNING: 'warning' as const, 13 | NO_RESPONSE: 'error' as const 14 | } 15 | 16 | export const getType = (code: keyof typeof STATUS_CODE_TO_TYPE) => { 17 | return STATUS_CODE_TO_TYPE[code] ?? 'unknown' 18 | } 19 | 20 | export const getText = (code: keyof typeof STATUS_CODE_TO_TYPE) => { 21 | if (code === 'OK') return code 22 | if (code === 'NO_RESPONSE') return 'No response' 23 | return capitalize(code) 24 | } 25 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/components/Property/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import cn from 'classnames' 9 | 10 | import styles from './styles.module.scss' 11 | 12 | type Props = { 13 | name: React.ReactNode 14 | children: React.ReactNode 15 | classes?: { 16 | name?: string 17 | content?: string 18 | } 19 | } 20 | 21 | export const Property = (props: Props) => { 22 | const { name, children } = props 23 | 24 | return ( 25 |
26 | 27 |
28 | {children} 29 |
30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/Info/components/Property/styles.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | margin-top: 4px; 4 | font-size: 12px; 5 | } 6 | 7 | .name { 8 | flex: 0 0 134px; 9 | margin-right: 14px; 10 | 11 | @media screen and (max-width: '600px') { 12 | flex: 1 1 100%; 13 | margin-right: 7px; 14 | } 15 | } 16 | 17 | .content { 18 | flex: 1 1 100%; 19 | font-weight: 700; 20 | width: 0; 21 | } 22 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/SnapshotsModal/utils.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { formatUTC } from '@postgres.ai/shared/utils/date' 9 | 10 | export const getTags = ({ date, pool }: { date: Date | null, pool: string | null }) => { 11 | const tags = [] 12 | 13 | if (date) tags.push({ name: 'Date', value: `${formatUTC(date, 'yyyy-MM-dd')} UTC` }) 14 | if (pool) tags.push({ name: 'Disk', value: pool }) 15 | 16 | return tags 17 | } 18 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/components/ClonesList/MenuCell/utils.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | export const destroyRestriction = (cloneId: string) => { 9 | const message = `The clone "${cloneId}" is marked as protected. To destroy it, disable the destroy protection first.` 10 | window.alert(message) 11 | } 12 | 13 | export const getResetApprove = (cloneId: string) => { 14 | const message = `Are you sure you want to reset the Database Lab clone: "${cloneId}"?` 15 | return window.confirm(message) 16 | } 17 | 18 | export const getDestroyApprove = (cloneId: string) => { 19 | const message = `Are you sure you want to destroy the Database Lab clone: "${cloneId}"?` 20 | return window.confirm(message) 21 | } 22 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/components/ClonesList/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import '@postgres.ai/shared/styles/vars'; 2 | 3 | .interactiveRow { 4 | cursor: pointer; 5 | } 6 | 7 | .verticalCentered { 8 | display: flex; 9 | align-items: center; 10 | } 11 | 12 | .infoIcon { 13 | margin-left: 4px; 14 | height: 12px; 15 | width: 12px; 16 | } 17 | 18 | .sortIcon { 19 | margin-left: 8px; 20 | width: 10px; 21 | } 22 | 23 | .protectionIcon { 24 | display: block; 25 | margin-left: 5px; 26 | height: 1em; 27 | color: $color-gray-semi-dark; 28 | } 29 | 30 | .emptyStub { 31 | margin-top: 16px; 32 | } 33 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/components/ModalReloadButton/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { makeStyles } from '@material-ui/core' 9 | 10 | import { Button } from '@postgres.ai/shared/components/Button2' 11 | 12 | type Props = { 13 | isReloading: boolean 14 | onReload: () => void 15 | } 16 | 17 | const useStyles = makeStyles( 18 | { 19 | spinner: { 20 | margin: '5px', 21 | }, 22 | content: { 23 | flex: '0 0 auto', 24 | alignSelf: 'flex-start', 25 | }, 26 | }, 27 | { index: 1 }, 28 | ) 29 | 30 | export const ModalReloadButton = (props: Props) => { 31 | const classes = useStyles() 32 | 33 | return ( 34 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/components/Tags/index.tsx: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import React from 'react' 9 | import { makeStyles } from '@material-ui/core' 10 | 11 | import { Tag } from './Tag' 12 | 13 | export type TagsProps = { 14 | data: { name: string; value: string }[] 15 | } 16 | 17 | const useStyles = makeStyles( 18 | { 19 | root: { 20 | display: 'flex', 21 | flexWrap: 'wrap', 22 | marginRight: '-8px', 23 | }, 24 | }, 25 | { index: 1 }, 26 | ) 27 | 28 | export const Tags = (props: TagsProps) => { 29 | const classes = useStyles() 30 | 31 | return ( 32 |
33 | {props.data.map((tag) => { 34 | return ( 35 | 36 | {tag.value} 37 | 38 | ) 39 | })} 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/context.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { createStrictContext } from '@postgres.ai/shared/utils/react' 9 | 10 | import { Api } from './stores/Main' 11 | import { Stores } from './useCreatedStores' 12 | 13 | export type Host = { 14 | instanceId: string 15 | routes: { 16 | createClone: () => string 17 | clone: (cloneId: string) => string 18 | } 19 | api: Api 20 | title: string 21 | callbacks?: { 22 | showDeprecatedApiBanner: () => void 23 | hideDeprecatedApiBanner: () => void 24 | } 25 | elements: { 26 | breadcrumbs: React.ReactNode 27 | } 28 | wsHost?: string 29 | isPlatform?: boolean 30 | setProjectAlias?: (alias: string) => void 31 | } 32 | 33 | // Host context. 34 | export const { useStrictContext: useHost, Provider: HostProvider } = 35 | createStrictContext() 36 | 37 | // Stores context. 38 | export const { useStrictContext: useStores, Provider: StoresProvider } = 39 | createStrictContext() 40 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/stores/ClonesModal.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { makeAutoObservable } from 'mobx' 9 | 10 | type FilterOptions = { 11 | pool?: string | null 12 | snapshotId?: string 13 | } 14 | 15 | export class ClonesModalStore { 16 | isOpenModal = false 17 | pool: string | null = null 18 | snapshotId: string | null = null 19 | 20 | constructor() { 21 | makeAutoObservable(this) 22 | } 23 | 24 | openModal = (filterOptions: FilterOptions | undefined = {}) => { 25 | const { pool = null, snapshotId = null } = filterOptions 26 | 27 | this.pool = pool 28 | this.snapshotId = snapshotId 29 | this.isOpenModal = true 30 | } 31 | 32 | closeModal = () => { 33 | this.isOpenModal = false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/stores/SnapshotsModal.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { makeAutoObservable } from 'mobx' 9 | 10 | type FilterOptions = { 11 | pool?: string | null 12 | date?: Date 13 | } 14 | 15 | export class SnapshotsModalStore { 16 | isOpenModal = false 17 | pool: string | null = null 18 | date: Date | null = null 19 | 20 | constructor() { 21 | makeAutoObservable(this) 22 | } 23 | 24 | openModal = (filterOptions: FilterOptions | undefined = {}) => { 25 | const { pool = null, date = null } = filterOptions 26 | 27 | this.pool = pool 28 | this.date = date 29 | this.isOpenModal = true 30 | } 31 | 32 | closeModal = () => { 33 | this.isOpenModal = false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/styles.scss: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2022, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | #logs-container { 9 | margin: 20px 0 0 0; 10 | border: 1px solid #b4b4b4; 11 | border-radius: 4px; 12 | overflow: hidden; 13 | padding: 0.5rem 1rem; 14 | 15 | & > p { 16 | font-size: small; 17 | font-family: 'Fira Code', monospace; 18 | padding: 1px 0; 19 | } 20 | } 21 | 22 | .error-log { 23 | color: red; 24 | } 25 | 26 | .snackbar-tag { 27 | position: fixed; 28 | bottom: 0; 29 | left: 50%; 30 | transform: translate(-50%, -50%); 31 | background-color: #fff2e5; 32 | color: #000; 33 | font-weight: 500; 34 | font-size: 11px; 35 | padding: 6px 8px; 36 | border-radius: 4px; 37 | transition: all 40050ms ease; 38 | cursor: pointer; 39 | z-index: 1; 40 | } 41 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Instance/useCreatedStores.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | import { MainStore } from './stores/Main' 4 | import { ClonesModalStore } from './stores/ClonesModal' 5 | import { SnapshotsModalStore } from './stores/SnapshotsModal' 6 | import { Host } from './context' 7 | 8 | export const useCreatedStores = (host: Host) => ({ 9 | main: useMemo(() => new MainStore(host.api), []), 10 | clonesModal: useMemo(() => new ClonesModalStore(), []), 11 | snapshotsModal: useMemo(() => new SnapshotsModalStore(), []), 12 | }) 13 | 14 | export type Stores = ReturnType 15 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Logs/Icons/PlusIcon.tsx: -------------------------------------------------------------------------------- 1 | export const PlusIcon = () => ( 2 | 3 | 7 | 8 | ) 9 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Logs/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const LOGS_NEW_DATA_MESSAGE = 2 | 'New data arrived below - scroll down to see it 👇🏻' 3 | 4 | export const LAPTOP_WIDTH_PX = 982 5 | export const LOGS_TIME_LIMIT = 20 6 | export const LOGS_LINE_LIMIT = 1000 7 | export const LOGS_ENDPOINT = '/instance/logs' 8 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Logs/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const stringWithoutBrackets = (val: string | undefined) => 2 | String(val).replace(/[\[\]]/g, '') 3 | 4 | export const stringContainsPattern = ( 5 | target: string, 6 | pattern = [ 7 | 'base.go', 8 | 'runners.go', 9 | 'snapshots.go', 10 | 'util.go', 11 | 'logging.go', 12 | 'ws.go', 13 | ], 14 | ) => { 15 | let value: number = 0 16 | pattern.forEach(function (word) { 17 | value = value + Number(target?.includes(word)) 18 | }) 19 | return value === 1 20 | } 21 | -------------------------------------------------------------------------------- /ui/packages/shared/pages/Logs/wsSnackbar.ts: -------------------------------------------------------------------------------- 1 | import { LOGS_NEW_DATA_MESSAGE } from '@postgres.ai/shared/pages/Logs/constants' 2 | 3 | export const wsSnackbar = (clientAtBottom: boolean, isNewData: boolean) => { 4 | const targetNode = document.getElementById('logs-container') 5 | const snackbarTag = document.createElement('div') 6 | 7 | if (!clientAtBottom && isNewData) { 8 | if (!targetNode?.querySelector('.snackbar-tag')) { 9 | targetNode?.appendChild(snackbarTag) 10 | snackbarTag.classList.add('snackbar-tag') 11 | if ( 12 | snackbarTag.childNodes.length === 0 && 13 | targetNode?.querySelector('p')?.textContent !== 'Not authorized' 14 | ) { 15 | snackbarTag.appendChild(document.createTextNode(LOGS_NEW_DATA_MESSAGE)) 16 | } 17 | snackbarTag.onclick = () => { 18 | targetNode?.scroll({ 19 | top: targetNode.scrollHeight, 20 | behavior: 'smooth', 21 | }) 22 | } 23 | } 24 | } else { 25 | targetNode?.querySelector('.snackbar-tag')?.remove() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ui/packages/shared/scripts/copy-assets.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const glob = require('glob'); 4 | 5 | const OUT_DIR = 'dist'; 6 | 7 | const PATTERNS = [ 8 | '**/*.scss', 9 | '**/*.module.scss', 10 | '**/*.json', 11 | 'react-app-env.d.ts', 12 | ]; 13 | 14 | const files = PATTERNS.flatMap(pattern => 15 | glob.sync(pattern, { 16 | cwd: '.', 17 | ignore: ['node_modules/**', 'dist/**'], 18 | nodir: true, 19 | }) 20 | ); 21 | 22 | files.forEach((file) => { 23 | const from = path.resolve(file); 24 | const to = path.join(OUT_DIR, file); 25 | const dir = path.dirname(to); 26 | fs.mkdirSync(dir, { recursive: true }); 27 | fs.copyFileSync(from, to); 28 | }); 29 | 30 | console.log(`✅ Copied ${files.length} assets to dist`); -------------------------------------------------------------------------------- /ui/packages/shared/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import './vars'; 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | font-family: $font-family-text; 11 | font-size: $font-size-main; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | code { 17 | font-family: $font-family-code; 18 | } 19 | 20 | button { 21 | font-family: inherit; 22 | font-size: inherit; 23 | } 24 | 25 | #root { 26 | height: 100vh; 27 | display: flex; 28 | flex-direction: column; 29 | } 30 | -------------------------------------------------------------------------------- /ui/packages/shared/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | // Screen width. 2 | @mixin screen-max-width ($width) { 3 | @media screen and (max-width: $width) { 4 | @content; 5 | } 6 | } 7 | 8 | @mixin sm { 9 | @include screen-max-width(600px) { 10 | @content; 11 | } 12 | } 13 | 14 | // Transitions. 15 | @mixin transition($properties, $duration, $function) { 16 | $transition: (); 17 | @for $i from 1 through length($properties) { 18 | $transition: append($transition, $duration nth($properties, $i) $function, $separator: comma); 19 | } 20 | 21 | transition: $transition; 22 | } 23 | 24 | @mixin touch-transition ($properties) { 25 | @include transition(($properties), .2s, ease-out); 26 | } 27 | 28 | @mixin animation-transition ($properties) { 29 | @include transition(($properties), .4s, ease-out); 30 | } 31 | -------------------------------------------------------------------------------- /ui/packages/shared/styles/vars.scss: -------------------------------------------------------------------------------- 1 | // Colors. 2 | $color-white: #fff; 3 | 4 | $color-white--100: #FBFBFB; 5 | 6 | $color-black: #000; 7 | 8 | $color-gray-100: #CCD7DA; 9 | $color-gray: #b4b4b4; 10 | 11 | $color-gray-semi-dark: #808080; 12 | 13 | $color-gray-dark: #444; 14 | 15 | $color-gray-darkest: #333; 16 | $color-gray-darkest--hover: darken($color-gray-darkest, 5); 17 | 18 | $color-orange: #ff6212; 19 | $color-orange--hover: darken($color-orange, 10); 20 | 21 | $color-status-ok: #0db94d; 22 | $color-status-warning: #fd8411; 23 | $color-status-error: #ff2020; 24 | $color-status-waiting: #ffad5f; 25 | $color-status-unknown: #c0c0c0; // prev color-gray 26 | 27 | 28 | $color-blue-main: #0F879D; 29 | $color-blue-dark: #026173; 30 | 31 | // Fonts. 32 | $font-family-text: 'Roboto', sans-serif; 33 | $font-family-code: 'Roboto Mono', monospace; 34 | 35 | // Typography. 36 | $font-size-small: 12px; 37 | $font-size-main: 14px; 38 | 39 | // Shadows. 40 | $box-shadow-layer: 0 2px 6px rgba(0, 0, 0, 0.1); 41 | 42 | // Sizes. 43 | $border-radius--small: 4px; 44 | -------------------------------------------------------------------------------- /ui/packages/shared/styles/vars.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | // Colors. 9 | const colorGray = '#c0c0c0' 10 | 11 | export const colors = { 12 | white: '#fff', 13 | gray: '#D6D6D6', 14 | 15 | status: { 16 | ok: '#0db94d', 17 | warning: '#fd8411', 18 | error: '#ff2020', 19 | waiting: '#ffad5f', 20 | unknown: colorGray 21 | } 22 | } 23 | 24 | // Mixins. 25 | export const createTransitionInteractive = (...props: string[]) => 26 | props.map((prop) => `${prop} .2s ease-out`).join(','); 27 | 28 | export const borderRadius = '4px'; 29 | 30 | export const resetStyles = { 31 | margin: 0, 32 | padding: 0, 33 | } 34 | 35 | export const resetStylesRoot = { 36 | ...resetStyles, 37 | '& *': { 38 | ...resetStyles 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ui/packages/shared/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist", 6 | "declaration": true, 7 | "emitDeclarationOnly": false, 8 | "noEmit": false, 9 | "module": "esnext", 10 | "target": "es2019", 11 | "moduleResolution": "node", 12 | "jsx": "react-jsx", 13 | "resolveJsonModule": true, 14 | "esModuleInterop": true 15 | }, 16 | "include": [ 17 | "components/**/*", 18 | "config/**/*", 19 | "helpers/**/*", 20 | "hooks/**/*", 21 | "icons/**/*", 22 | "pages/**/*", 23 | "stores/**/*", 24 | "styles/**/*", 25 | "types/**/*", 26 | "utils/**/*", 27 | "react-app-env.d.ts", 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "dist", 32 | "meta.json", 33 | "craco.config.js", 34 | "**/*.test.ts", 35 | "**/*.test.tsx" 36 | ] 37 | } -------------------------------------------------------------------------------- /ui/packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": ".", 23 | "paths": { 24 | "@postgres.ai/shared/*": ["./*"] 25 | } 26 | }, 27 | "include": [ 28 | "." 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/createClone.ts: -------------------------------------------------------------------------------- 1 | import { Clone } from '@postgres.ai/shared/types/api/entities/clone' 2 | 3 | export type CreateClone = (args: { 4 | instanceId: string 5 | cloneId: string 6 | snapshotId: string 7 | dbUser: string 8 | dbPassword: string 9 | isProtected: boolean 10 | }) => Promise<{ response: Clone | null; error: Response | null }> 11 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/destroyClone.ts: -------------------------------------------------------------------------------- 1 | export type DestroyClone = (args: { 2 | instanceId: string 3 | cloneId: string 4 | }) => Promise<{ 5 | response: true | null 6 | error: Response | null 7 | }> 8 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/getClone.ts: -------------------------------------------------------------------------------- 1 | import { Clone } from '@postgres.ai/shared/types/api/entities/clone' 2 | 3 | export type GetClone = (args: { 4 | instanceId: string 5 | cloneId: string 6 | }) => Promise<{ response: Clone | null; error: Response | null }> 7 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/getConfig.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "../entities/config" 2 | 3 | export type GetConfig = () => Promise<{ 4 | response: Config | null 5 | error: Response | null 6 | }> 7 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/getEngine.ts: -------------------------------------------------------------------------------- 1 | export type EngineDto = { 2 | version: string 3 | edition?: string 4 | } 5 | 6 | export type GetEngine = () => Promise<{ 7 | response: EngineType | null 8 | error: Response | null 9 | }> 10 | 11 | export const formatEngineDto = (dto: EngineDto) => dto 12 | 13 | export type EngineType = ReturnType 14 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/getFullConfig.ts: -------------------------------------------------------------------------------- 1 | export type GetFullConfig = () => Promise<{ 2 | response: string | null 3 | error: Response | any | null 4 | }> 5 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/getInstance.ts: -------------------------------------------------------------------------------- 1 | import { Instance } from '@postgres.ai/shared/types/api/entities/instance' 2 | 3 | export type GetInstance = (args: { instanceId: string }) => Promise<{ 4 | response: Instance | null 5 | error: Response | null 6 | }> 7 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/getInstanceRetrieval.ts: -------------------------------------------------------------------------------- 1 | import { InstanceRetrievalType } from '@postgres.ai/shared/types/api/entities/instanceRetrieval' 2 | 3 | export type GetInstanceRetrieval = (args: { instanceId: string }) => Promise<{ 4 | response: InstanceRetrievalType | null 5 | error: Response | null 6 | }> 7 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/getSeImages.ts: -------------------------------------------------------------------------------- 1 | export type GetSeImages = (args: { 2 | packageGroup: string 3 | platformUrl?: string 4 | }) => Promise<{ 5 | response: InstanceRetrievalType | null 6 | error: Response | null 7 | }> 8 | 9 | export interface SeImages { 10 | org_id?: number 11 | package_group: string 12 | pg_major_version: string 13 | tag: string 14 | pg_config_presets?: { 15 | shared_preload_libraries: string 16 | } 17 | location: string 18 | } 19 | 20 | export const formatSeImages = (seImages: SeImages[]) => seImages 21 | 22 | export type InstanceRetrievalType = ReturnType 23 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/getSnapshots.ts: -------------------------------------------------------------------------------- 1 | import { Snapshot } from '@postgres.ai/shared/types/api/entities/snapshot' 2 | 3 | export type GetSnapshots = (args: { instanceId: string }) => Promise<{ 4 | response: Snapshot[] | null 5 | error: Response | null 6 | }> 7 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/getWSToken.ts: -------------------------------------------------------------------------------- 1 | import { WSToken } from '@postgres.ai/shared/types/api/entities/wsToken' 2 | 3 | export type GetWSToken = (args: { instanceId: string }) => Promise<{ 4 | response: WSToken | null 5 | error: Response | null 6 | }> 7 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/initWS.ts: -------------------------------------------------------------------------------- 1 | export type InitWS = ( path: string, token: string ) => WebSocket 2 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/refreshInstance.ts: -------------------------------------------------------------------------------- 1 | export type RefreshInstance = (args: { instanceId: string }) => Promise<{ 2 | response: boolean | null 3 | error: Response | null 4 | }> 5 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/resetClone.ts: -------------------------------------------------------------------------------- 1 | export type ResetClone = (args: { 2 | instanceId: string 3 | cloneId: string 4 | snapshotId: string 5 | }) => Promise<{ 6 | response: true | null 7 | error: Response | null 8 | }> 9 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/updateClone.ts: -------------------------------------------------------------------------------- 1 | export type UpdateClone = (args: { 2 | instanceId: string 3 | cloneId: string 4 | clone: { 5 | isProtected: boolean 6 | } 7 | }) => Promise<{ 8 | response: true | null 9 | error: Response | null 10 | }> 11 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/endpoints/updateConfig.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@postgres.ai/shared/types/api/entities/config' 2 | 3 | export type UpdateConfig = (values: Config) => Promise<{ 4 | response: Response | null 5 | error: Response | null 6 | }> 7 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/entities/dbSource.ts: -------------------------------------------------------------------------------- 1 | export type dbSource = { 2 | host: string 3 | port: string 4 | dbname: string 5 | username: string 6 | password: string 7 | db_list?: string[] 8 | } 9 | 10 | export type TestSourceDTO = { 11 | message: string 12 | status: string 13 | } -------------------------------------------------------------------------------- /ui/packages/shared/types/api/entities/pool.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | export type PoolDto = { 9 | cloneList: string[] 10 | fileSystem: { 11 | compressRatio: number 12 | dataSize: number 13 | free: number 14 | mode: string 15 | size: number 16 | used: number 17 | usedByClones: number 18 | usedBySnapshots: number 19 | } 20 | mode: string 21 | name: string 22 | status: 'active' | 'empty' | 'refreshing' 23 | } 24 | 25 | export const formatPoolDto = (dto: PoolDto) => dto 26 | 27 | export type Pool = ReturnType 28 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/entities/snapshot.ts: -------------------------------------------------------------------------------- 1 | import { parseDate } from '@postgres.ai/shared/utils/date' 2 | 3 | export type SnapshotDto = { 4 | createdAt: string 5 | dataStateAt: string 6 | id: string 7 | pool: string 8 | physicalSize: number 9 | logicalSize: number 10 | } 11 | 12 | export const formatSnapshotDto = (dto: SnapshotDto) => ({ 13 | ...dto, 14 | createdAtDate: parseDate(dto.createdAt), 15 | dataStateAtDate: parseDate(dto.dataStateAt) 16 | }) 17 | 18 | export type Snapshot = ReturnType 19 | -------------------------------------------------------------------------------- /ui/packages/shared/types/api/entities/wsToken.ts: -------------------------------------------------------------------------------- 1 | export type WSTokenDTO = { 2 | token: string 3 | } 4 | 5 | export const formatWSTokenDto = (dto: WSTokenDTO) => dto 6 | 7 | export type WSToken = ReturnType 8 | -------------------------------------------------------------------------------- /ui/packages/shared/types/byte-size/index.d.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | type Options = { 9 | precision: number 10 | units: 'metric' | 'iec' | 'metric_octet' | 'iec_octet' 11 | } 12 | 13 | type Result = { 14 | value: string 15 | unit: string 16 | long: string 17 | } 18 | 19 | declare module 'byte-size' { 20 | declare const byteSize: (bytes: number, options?: Options) => Result 21 | export default byteSize 22 | } 23 | -------------------------------------------------------------------------------- /ui/packages/shared/utils/api.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | export const getTextFromUnknownApiError = async (error: Response) => { 9 | const log = (text: string) => console.error('Unknown API error', text) 10 | 11 | try { 12 | const result = await error.json() 13 | log(result) 14 | return JSON.stringify(result) 15 | } catch (e) { 16 | // not a json 17 | } 18 | 19 | try { 20 | const result = await error.text() 21 | log(result) 22 | return result 23 | } catch (e) { 24 | // not a text 25 | } 26 | 27 | const result = `${error.status} ${error.statusText}` 28 | log(result) 29 | return result 30 | } 31 | -------------------------------------------------------------------------------- /ui/packages/shared/utils/clone.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import { Clone } from '@postgres.ai/shared/types/api/entities/clone' 9 | import { capitalize } from '@postgres.ai/shared/utils/strings' 10 | 11 | const STATUS_CODE_TO_TYPE = { 12 | OK: 'ok' as const, 13 | CREATING: 'waiting' as const, 14 | DELETING: 'waiting' as const, 15 | RESETTING: 'waiting' as const, 16 | FATAL: 'error' as const, 17 | } 18 | 19 | type StatusCode = keyof typeof STATUS_CODE_TO_TYPE 20 | 21 | export const getCloneStatusType = (statusCode: StatusCode) => 22 | STATUS_CODE_TO_TYPE[statusCode] ?? 'unknown' 23 | 24 | export const getCloneStatusText = (statusCode: StatusCode) => { 25 | if (statusCode === 'OK') return statusCode 26 | 27 | return capitalize(statusCode) 28 | } 29 | 30 | export const checkIsCloneStable = (clone: Clone) => 31 | clone.status.code === 'OK' || clone.status.code === 'FATAL' 32 | -------------------------------------------------------------------------------- /ui/packages/shared/utils/instance.ts: -------------------------------------------------------------------------------- 1 | export const parseEngineVersion = (str: string) => { 2 | const [versionStr] = str.split('-') 3 | const [major, minor, patch] = versionStr.split('.') 4 | if (!major || !minor || !patch) return null 5 | return { 6 | major: Number(major), 7 | minor: Number(minor), 8 | patch: Number(patch), 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ui/packages/shared/utils/numbers.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | export const round = (value: number, precision = 0) => { 9 | const multiplier = 10 ** precision 10 | return Math.round(value * multiplier) / multiplier 11 | } 12 | -------------------------------------------------------------------------------- /ui/packages/shared/utils/react.ts: -------------------------------------------------------------------------------- 1 | import { useContext, createContext } from 'react' 2 | 3 | export const createStrictContext = () => { 4 | const Context = createContext(undefined as unknown as Value) 5 | 6 | return { 7 | useStrictContext: () => useContext(Context), 8 | Provider: Context.Provider, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ui/packages/shared/utils/snapshot.ts: -------------------------------------------------------------------------------- 1 | import { Snapshot } from '@postgres.ai/shared/types/api/entities/snapshot' 2 | 3 | export const compareSnapshotsDesc = (a: Snapshot, b: Snapshot) => 4 | b.dataStateAtDate.getTime() - a.dataStateAtDate.getTime() 5 | -------------------------------------------------------------------------------- /ui/packages/shared/utils/strings.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | export const capitalize = (value: string) => { 9 | const [firstChar, ...otherChars] = value.split('') 10 | return `${firstChar.toUpperCase()}${otherChars.join('').toLowerCase()}` 11 | } 12 | -------------------------------------------------------------------------------- /ui/packages/shared/utils/units.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai 3 | * All Rights Reserved. Proprietary and confidential. 4 | * Unauthorized copying of this file, via any medium is strictly prohibited 5 | *-------------------------------------------------------------------------- 6 | */ 7 | 8 | import byteSize from 'byte-size' 9 | 10 | type Options = { 11 | precision?: number 12 | } 13 | 14 | export const formatBytesIEC = (bytes: number, options?: Options) => { 15 | const { precision = 3 } = options ?? {} 16 | 17 | const result = byteSize(bytes, { 18 | precision, 19 | units: 'iec', 20 | }) 21 | 22 | return `${result.value} ${result.unit}` 23 | } 24 | -------------------------------------------------------------------------------- /ui/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # include packages in subfolders (e.g. apps/ and packages/) 3 | - 'packages/*' 4 | # if required, exclude some directories 5 | - '!**/test/**' 6 | --------------------------------------------------------------------------------