diff --git a/ui/packages/ce/cypress/e2e/tabs.cy.js b/ui/packages/ce/cypress/e2e/tabs.cy.js index d9c6dc59ff579ecb26dd56ed5644056f305dc995..db2afe821cc2753fc58581ba9c07e2b2844bd15a 100644 --- a/ui/packages/ce/cypress/e2e/tabs.cy.js +++ b/ui/packages/ce/cypress/e2e/tabs.cy.js @@ -4,7 +4,6 @@ Cypress.on('uncaught:exception', () => { return false }) -// Function to set up intercepts for the requests function setupIntercepts() { const exceptions = [ '/healthz', @@ -44,7 +43,6 @@ function setupIntercepts() { }, }) - // Intercept all fetch requests and return a 200 cy.intercept('GET', '*', (req) => { if ( req.resourceType === 'fetch' && @@ -61,7 +59,6 @@ function setupIntercepts() { } describe('Configuration tab', () => { - // It should intercept the requests beforeEach(() => { setupIntercepts() }) @@ -71,19 +68,10 @@ describe('Configuration tab', () => { retryOnStatusCodeFailure: true, onLoad: () => { cy.get('.MuiTabs-flexContainer') - .contains('Configuration', { timeout: 10000 }) + .contains('Configuration') .should('be.visible') .click({ force: true }) }, }) }) - - it('should have form inputs in the "Configuration" tab', () => { - cy.get('.MuiTabs-flexContainer') - .contains('Configuration', { timeout: 10000 }) - .should('be.visible') - .click({ force: true }) - - cy.get('button[type="button"]').should('exist') - }) }) diff --git a/ui/packages/ce/src/App/Instance/Branches/CreateBranch/index.tsx b/ui/packages/ce/src/App/Instance/Branches/CreateBranch/index.tsx index 67ce567940cfeca154a3087d7f1b64ebf2b5be86..8a5fe3ae092c961fc0263404f3a53e64a8310d71 100644 --- a/ui/packages/ce/src/App/Instance/Branches/CreateBranch/index.tsx +++ b/ui/packages/ce/src/App/Instance/Branches/CreateBranch/index.tsx @@ -1,6 +1,6 @@ import { getBranches } from 'api/branches/getBranches' import { createBranch } from 'api/branches/createBranch' -import { getSnapshotList } from 'api/branches/getSnapshotList' +import { getBranchSnapshots } from 'api/snapshots/getBranchSnapshots' import { CreateBranchPage } from '@postgres.ai/shared/pages/CreateBranch' @@ -12,7 +12,7 @@ export const CreateBranch = () => { const api = { getBranches, createBranch, - getSnapshotList, + getBranchSnapshots, } const elements = { diff --git a/ui/packages/ce/src/App/Instance/Clones/CreateClone/index.tsx b/ui/packages/ce/src/App/Instance/Clones/CreateClone/index.tsx index 0bdabe56c4d89cda63c8bafdd89dd2bbdff3bbf1..41455b358af30e1df3a6eac9b50df9b4f6388f51 100644 --- a/ui/packages/ce/src/App/Instance/Clones/CreateClone/index.tsx +++ b/ui/packages/ce/src/App/Instance/Clones/CreateClone/index.tsx @@ -5,10 +5,10 @@ import { NavPath } from 'components/NavPath' import { ROUTES } from 'config/routes' import { getInstance } from 'api/instances/getInstance' import { getInstanceRetrieval } from 'api/instances/getInstanceRetrieval' -import { getSnapshots } from 'api/snapshots/getSnapshots' import { createClone } from 'api/clones/createClone' import { getClone } from 'api/clones/getClone' import { getBranches } from 'api/branches/getBranches' +import { getBranchSnapshots } from 'api/snapshots/getBranchSnapshots' export const CreateClone = () => { const routes = { @@ -17,12 +17,12 @@ export const CreateClone = () => { } const api = { - getSnapshots, getInstance, getInstanceRetrieval, createClone, getClone, getBranches, + getBranchSnapshots, } const elements = { diff --git a/ui/packages/ce/src/api/branches/deleteBranch.ts b/ui/packages/ce/src/api/branches/deleteBranch.ts index b9cae5132f427fb1f46df76b4fa8c04b78d76fde..d2a335ac5a21787dad1f7f48739af3715203e331 100644 --- a/ui/packages/ce/src/api/branches/deleteBranch.ts +++ b/ui/packages/ce/src/api/branches/deleteBranch.ts @@ -11,12 +11,12 @@ export const deleteBranch = async (branchName: string) => { const response = await request('/branch/delete', { method: 'POST', body: JSON.stringify({ - branchName: branchName, + branchName, }), }) return { response: response.ok ? await response.json() : null, - error: response.ok ? null : response, + error: response.ok ? null : await response.json(), } } diff --git a/ui/packages/ce/src/api/branches/getBranches.ts b/ui/packages/ce/src/api/branches/getBranches.ts index a9f351ba7c8adb903e0150351b817288b212c3bd..c8185e235f1d812d303a09d6561f7adb04f6d91d 100644 --- a/ui/packages/ce/src/api/branches/getBranches.ts +++ b/ui/packages/ce/src/api/branches/getBranches.ts @@ -6,12 +6,13 @@ */ import { request } from 'helpers/request' +import { formatBranchesDto } from '@postgres.ai/shared/types/api/endpoints/getBranches' export const getBranches = async () => { const response = await request(`/branches`) return { - response: response.ok ? await response.json() : null, + response: response.ok ? formatBranchesDto(await response.json()) : null, error: response.ok ? null : response, } } diff --git a/ui/packages/ce/src/api/snapshots/destroySnapshot.ts b/ui/packages/ce/src/api/snapshots/destroySnapshot.js similarity index 80% rename from ui/packages/ce/src/api/snapshots/destroySnapshot.ts rename to ui/packages/ce/src/api/snapshots/destroySnapshot.js index c18a521758a429da043ddee1d7d655073ae72169..e68699543923c05eb82390fede1438510c97e55c 100644 --- a/ui/packages/ce/src/api/snapshots/destroySnapshot.ts +++ b/ui/packages/ce/src/api/snapshots/destroySnapshot.js @@ -5,11 +5,9 @@ *-------------------------------------------------------------------------- */ -import { DestroySnapshot } from '@postgres.ai/shared/types/api/endpoints/destroySnapshot' - import { request } from 'helpers/request' -export const destroySnapshot: DestroySnapshot = async (snapshotId) => { +export const destroySnapshot = async (snapshotId) => { const response = await request(`/snapshot/delete`, { method: 'POST', body: JSON.stringify({ diff --git a/ui/packages/ce/src/api/snapshots/getBranchSnapshots.ts b/ui/packages/ce/src/api/snapshots/getBranchSnapshots.ts new file mode 100644 index 0000000000000000000000000000000000000000..de4029b3c08d2f0d4c7ae921aa240671cfd54845 --- /dev/null +++ b/ui/packages/ce/src/api/snapshots/getBranchSnapshots.ts @@ -0,0 +1,17 @@ +/*-------------------------------------------------------------------------- + * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai + * All Rights Reserved. Proprietary and confidential. + * Unauthorized copying of this file, via any medium is strictly prohibited + *-------------------------------------------------------------------------- + */ + +import { request } from 'helpers/request' + +export const getBranchSnapshots = async (branch: string) => { + const response = await request(`/branch/snapshots/${branch}`) + + return { + response: response.ok ? await response.json() : null, + error: response.ok ? null : response, + } +} diff --git a/ui/packages/shared/components/DestroyCloneModal/index.tsx b/ui/packages/shared/components/DestroyCloneModal/index.tsx index ecfff306a1ee2bb140d16f15c8bbf2c7babe3bb4..d1a76f1896fe55cc97da6b980d5a62539fb5bd3b 100644 --- a/ui/packages/shared/components/DestroyCloneModal/index.tsx +++ b/ui/packages/shared/components/DestroyCloneModal/index.tsx @@ -28,16 +28,12 @@ export const DestroyCloneModal = (props: Props) => { } return ( - + Are you sure you want to destroy clone{' '} - {cloneId}? + {cloneId}? This action cannot be undone. - + { text: 'Destroy clone', variant: 'primary', onClick: handleClickDestroy, - } + }, ]} /> diff --git a/ui/packages/shared/pages/Branches/Branch/index.tsx b/ui/packages/shared/pages/Branches/Branch/index.tsx index 7668d9726c0126b3bbda574e115a7747dd14d76f..f835661c21cd1c4a09f8cd2321e1a038d2631ac7 100644 --- a/ui/packages/shared/pages/Branches/Branch/index.tsx +++ b/ui/packages/shared/pages/Branches/Branch/index.tsx @@ -40,6 +40,7 @@ import { import { useCreatedStores } from './useCreatedStores' import { Host } from './context' +import { DeleteBranch } from 'types/api/endpoints/deleteBranch' type Props = Host @@ -152,15 +153,9 @@ export const BranchesPage = observer((props: Props) => { isBranchesLoading, getBranchesError, snapshotListError, - deleteBranchError, getBranchError, } = stores.main - const handleDestroyBranch = async () => { - const isSuccess = await deleteBranch(props.branchId) - if (isSuccess) history.push(props.routes.branch()) - } - const hasBranchError = getBranchesError || getBranchError || snapshotListError const branchLogLength = snapshotList?.reduce((acc, snapshot) => { @@ -359,11 +354,11 @@ export const BranchesPage = observer((props: Props) => { className={classes.marginTop} tag="h2" level={2} - text={'Delete branch using CLI'} + text={'Destroy branch using CLI'} />

- You can delete this branch using CLI. To do this, run the - command below: + You can destroy this branch using CLI. To do this, run the command + below:

@@ -374,8 +369,8 @@ export const BranchesPage = observer((props: Props) => { text={'Get branches using CLI'} />

- You can get a list of all branches using CLI. Copy the command - below and paste it into your terminal. + To list all branches using CLI, copy + and paste it into your terminal.

@@ -394,8 +389,7 @@ export const BranchesPage = observer((props: Props) => { setIsOpenDestroyModal(false)} - deleteBranchError={deleteBranchError} - deleteBranch={handleDestroyBranch} + deleteBranch={deleteBranch as DeleteBranch} branchName={props.branchId} /> diff --git a/ui/packages/shared/pages/Branches/Branch/stores/Main.ts b/ui/packages/shared/pages/Branches/Branch/stores/Main.ts index 052b9f9df7a833dd9c0286b000341edc9e88f4da..6e28532c198d5a6384ed77bd1a50e583728f1abc 100644 --- a/ui/packages/shared/pages/Branches/Branch/stores/Main.ts +++ b/ui/packages/shared/pages/Branches/Branch/stores/Main.ts @@ -8,11 +8,11 @@ import { makeAutoObservable } from 'mobx' import { GetBranches } from '@postgres.ai/shared/types/api/endpoints/getBranches' -import { GetBranchesResponseType } from '@postgres.ai/shared/types/api/endpoints/getBranches' +import { Branch } from '@postgres.ai/shared/types/api/endpoints/getBranches' import { DeleteBranch } from '@postgres.ai/shared/types/api/endpoints/deleteBranch' import { + SnapshotList, GetSnapshotList, - GetSnapshotListResponseType, } from '@postgres.ai/shared/types/api/endpoints/getSnapshotList' type Error = { @@ -29,15 +29,14 @@ export type Api = { export class MainStore { getBranchError: Error | null = null snapshotListError: Error | null = null - deleteBranchError: Error | null = null getBranchesError: Error | null = null isReloading = false isBranchesLoading = false - branches: GetBranchesResponseType[] = [] - branch: GetBranchesResponseType | null = null - snapshotList: GetSnapshotListResponseType[] | null = null + branches: Branch[] | null = null + branch: Branch | null = null + snapshotList: SnapshotList[] | null = null private readonly api: Api @@ -100,15 +99,10 @@ export class MainStore { deleteBranch = async (branchName: string) => { if (!branchName) return - this.deleteBranchError = null - const { response, error } = await this.api.deleteBranch(branchName) - if (error) { - this.deleteBranchError = await error.json().then((err) => err) - } - return response + return { response, error } } getSnapshotList = async (branchName: string) => { diff --git a/ui/packages/shared/pages/Branches/components/BranchesTable/index.tsx b/ui/packages/shared/pages/Branches/components/BranchesTable/index.tsx index 280ea27ce1f3e9ea6d4d07b7bdbaf7d2e2e20ad7..8660202eec822170681d6051b7bf276e36c05aa8 100644 --- a/ui/packages/shared/pages/Branches/components/BranchesTable/index.tsx +++ b/ui/packages/shared/pages/Branches/components/BranchesTable/index.tsx @@ -10,9 +10,11 @@ import { useEffect, useState } from 'react' import copy from 'copy-to-clipboard' import { makeStyles } from '@material-ui/core' import { useHistory } from 'react-router-dom' +import { formatDistanceToNowStrict } from 'date-fns' +import { isValidDate } from '@postgres.ai/shared/utils/date' import { ArrowDropDownIcon } from '@postgres.ai/shared/icons/ArrowDropDown' -import { GetBranchesResponseType } from '@postgres.ai/shared/types/api/endpoints/getBranches' +import { Branch } from '@postgres.ai/shared/types/api/endpoints/getBranches' import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer' import { Table, @@ -25,6 +27,7 @@ import { } from '@postgres.ai/shared/components/Table' import { DeleteBranchModal } from '../Modals/DeleteBranchModal' +import { DeleteBranch } from 'types/api/endpoints/deleteBranch' const useStyles = makeStyles( { @@ -57,22 +60,20 @@ const useStyles = makeStyles( ) export const BranchesTable = ({ - branchesData, + branches, emptyTableText, deleteBranch, - deleteBranchError, }: { - branchesData: GetBranchesResponseType[] + branches: Branch[] emptyTableText: string - deleteBranch: (branchId: string) => void - deleteBranchError: { title?: string; message?: string } | null + deleteBranch: DeleteBranch }) => { const history = useHistory() const classes = useStyles() const [state, setState] = useState({ sortByParent: 'desc', - branches: [] as GetBranchesResponseType[], + branches: [] as Branch[], }) const [branchId, setBranchId] = useState('') const [isOpenDestroyModal, setIsOpenDestroyModal] = useState(false) @@ -97,9 +98,9 @@ export const BranchesTable = ({ useEffect(() => { setState({ sortByParent: 'desc', - branches: branchesData ?? [], + branches: branches ?? [], }) - }, [branchesData]) + }, [branches]) if (!state.branches.length) { return

{emptyTableText}

@@ -145,7 +146,7 @@ export const BranchesTable = ({ onClick: () => copy(branch.snapshotID), }, { - name: 'Delete branch', + name: 'Destroy branch', onClick: () => { setBranchId(branch.name) setIsOpenDestroyModal(true) @@ -156,7 +157,15 @@ export const BranchesTable = ({ {branch.name} {branch.parent} - {branch.dataStateAt || '-'} + + {branch.dataStateAt} ( + {isValidDate(new Date(branch.dataStateAt)) + ? formatDistanceToNowStrict(new Date(branch.dataStateAt), { + addSuffix: true, + }) + : '-'} + ) + {branch.snapshotID} ))} @@ -167,7 +176,6 @@ export const BranchesTable = ({ setIsOpenDestroyModal(false) setBranchId('') }} - deleteBranchError={deleteBranchError} deleteBranch={deleteBranch} branchName={branchId} /> diff --git a/ui/packages/shared/pages/Branches/components/Modals/DeleteBranchModal/index.tsx b/ui/packages/shared/pages/Branches/components/Modals/DeleteBranchModal/index.tsx index 3aaae4c5da192b30905d3555d9127018a1585ffa..9d49ce224fb8b307d1d43d0fa4cedd96b1325604 100644 --- a/ui/packages/shared/pages/Branches/components/Modals/DeleteBranchModal/index.tsx +++ b/ui/packages/shared/pages/Branches/components/Modals/DeleteBranchModal/index.tsx @@ -5,7 +5,7 @@ *-------------------------------------------------------------------------- */ -import { useEffect, useState } from 'react' +import { useState } from 'react' import { makeStyles } from '@material-ui/core' import { Modal } from '@postgres.ai/shared/components/Modal' @@ -13,9 +13,9 @@ import { ModalProps } from '@postgres.ai/shared/pages/Branches/components/Modals import { SimpleModalControls } from '@postgres.ai/shared/components/SimpleModalControls' import { ImportantText } from '@postgres.ai/shared/components/ImportantText' import { Text } from '@postgres.ai/shared/components/Text' +import { DeleteBranch } from 'types/api/endpoints/deleteBranch' interface DeleteBranchModalProps extends ModalProps { - deleteBranchError: { title?: string; message?: string } | null - deleteBranch: (branchName: string) => void + deleteBranch: DeleteBranch branchName: string } @@ -32,31 +32,33 @@ const useStyles = makeStyles( export const DeleteBranchModal = ({ isOpen, onClose, - deleteBranchError, deleteBranch, branchName, }: DeleteBranchModalProps) => { const classes = useStyles() - const [deleteError, setDeleteError] = useState(deleteBranchError?.message) + const [deleteError, setDeleteError] = useState(null) - const handleSubmit = () => { - deleteBranch(branchName) + const handleDelete = async () => { + const deleteRes = await deleteBranch(branchName) + + if (deleteRes?.error) { + setDeleteError(deleteRes.error?.message) + } else { + window.location.reload() + } } const handleClose = () => { - setDeleteError('') + setDeleteError(null) onClose() } - useEffect(() => { - setDeleteError(deleteBranchError?.message) - }, [deleteBranchError]) - return ( Are you sure you want to destroy branch{' '} - {branchName}? + {branchName}? This action cannot be + undone. {deleteError &&

{deleteError}

} { const stores = useStores() const classes = useStyles() const history = useHistory() - const [branchesList, setBranchesList] = useState( - [], - ) + const [branches, setBranches] = useState([]) const { instance, getBranches, isBranchesLoading, getBranchesError, - deleteBranchError, deleteBranch, } = stores.main const goToBranchAddPage = () => history.push(host.routes.createBranch()) - const handleDestroyBranch = async (branchId: string) => { - const isSuccess = await deleteBranch(branchId) - if (isSuccess) history.push('/instance/branches') - } - useEffect(() => { getBranches().then((response) => { - response && setBranchesList(response) + response && setBranches(response) }) }, []) @@ -89,18 +82,18 @@ export const Branches = observer((): React.ReactElement => { - {!branchesList.length && ( + {!branches.length && (
@@ -111,10 +104,9 @@ export const Branches = observer((): React.ReactElement => { } /> )} diff --git a/ui/packages/shared/pages/Clone/index.tsx b/ui/packages/shared/pages/Clone/index.tsx index 6fb6313caa225a5ee333258d74311de55026486d..05fd09e54d0685158a44b38cbd6a1f996f67d987 100644 --- a/ui/packages/shared/pages/Clone/index.tsx +++ b/ui/packages/shared/pages/Clone/index.tsx @@ -623,12 +623,12 @@ export const Clone = observer((props: Props) => { />
- When enabled no one can delete this clone and automated deletion + When enabled no one can destroy this clone and automated deletion is also disabled.
Please be careful: abandoned clones with this checkbox enabled may cause out-of-disk-space events. Check disk space on daily basis - and delete this clone once the work is done. + and destroy this clone once the work is done.

{stores.main.updateCloneError && ( diff --git a/ui/packages/shared/pages/CreateBranch/index.tsx b/ui/packages/shared/pages/CreateBranch/index.tsx index 5e4346ca49a014714af38d030a3a4fa2a970a9cc..4ebab839c3a2391c9e1f0d93e311751b10379318 100644 --- a/ui/packages/shared/pages/CreateBranch/index.tsx +++ b/ui/packages/shared/pages/CreateBranch/index.tsx @@ -8,7 +8,7 @@ import cn from 'classnames' import { useHistory } from 'react-router' import { observer } from 'mobx-react-lite' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { TextField, makeStyles } from '@material-ui/core' import { Button } from '@postgres.ai/shared/components/Button' @@ -25,6 +25,7 @@ import { useForm } from './useForm' import { MainStoreApi } from './stores/Main' import { useCreatedStores } from './useCreatedStores' import { getCliBranchListCommand, getCliCreateBranchCommand } from './utils' +import { Snapshot } from 'types/api/entities/snapshot' interface CreateBranchProps { api: MainStoreApi @@ -94,17 +95,18 @@ export const CreateBranchPage = observer( const stores = useCreatedStores(api) const classes = useStyles() const history = useHistory() + const [branchSnapshots, setBranchSnapshots] = useState([]) const { load, - snapshotListError, + branchesList, getBranchesError, createBranch, createBranchError, isBranchesLoading, isCreatingBranch, - branchesList, - snapshotsList, + getBranchSnapshots, + branchSnapshotsError, } = stores.main const handleSubmit = async (values: CreateBranchFormValues) => { @@ -115,18 +117,25 @@ export const CreateBranchPage = observer( }) } + const handleParentBranchChange = async ( + e: React.ChangeEvent, + ) => { + const branchName = e.target.value + formik.setFieldValue('baseBranch', branchName) + await getBranchSnapshots(branchName).then((response) => { + if (response) { + setBranchSnapshots(response) + formik.setFieldValue('snapshotID', response[0]?.id) + } + }) + } + const [{ formik }] = useForm(handleSubmit) useEffect(() => { - load(formik.values.baseBranch) + load() }, [formik.values.baseBranch]) - useEffect(() => { - if (snapshotsList?.length) { - formik.setFieldValue('snapshotID', snapshotsList[0]?.id) - } - }, [snapshotsList]) - if (isBranchesLoading) { return } @@ -137,11 +146,11 @@ export const CreateBranchPage = observer(
- {(snapshotListError || getBranchesError) && ( + {(branchSnapshotsError || getBranchesError) && (
@@ -174,9 +183,7 @@ export const CreateBranchPage = observer( label="Parent branch" value={formik.values.baseBranch} disabled={!branchesList || formik.isSubmitting} - onChange={(e) => - formik.setFieldValue('baseBranch', e.target.value) - } + onChange={handleParentBranchChange} error={Boolean(formik.errors.baseBranch)} items={ branchesList @@ -206,8 +213,8 @@ export const CreateBranchPage = observer( } error={Boolean(formik.errors.baseBranch)} items={ - snapshotsList - ? snapshotsList.map((snapshot, i) => { + branchSnapshots + ? branchSnapshots.map((snapshot, i) => { const isLatest = i === 0 return { value: snapshot.id, @@ -252,7 +259,10 @@ export const CreateBranchPage = observer( form, copy the command below and paste it into your terminal.

{ - await this.getBranches() - .then((response) => { - if (response) { - this.branchesList = response - } - }) - .then(() => { - this.getSnapshotList(baseBranch).then((res) => { - if (res) { - const filteredSnapshots = res.filter((snapshot) => snapshot.id) - this.snapshotsList = filteredSnapshots - } - }) - }) + load = async () => { + await this.getBranches().then((response) => { + if (response) { + this.branchesList = response + } + }) } createBranch = async (values: CreateBranchFormValues) => { @@ -84,20 +71,20 @@ export class MainStore { const { response, error } = await this.api.getBranches() + this.isBranchesLoading = false + if (error) this.getBranchesError = await error.json().then((err) => err) return response } - getSnapshotList = async (branchName: string) => { - if (!this.api.getSnapshotList) return + getBranchSnapshots = async (branchName: string) => { + if (!this.api.getBranchSnapshots) return - const { response, error } = await this.api.getSnapshotList(branchName) - - this.isBranchesLoading = false + const { response, error } = await this.api.getBranchSnapshots(branchName) if (error) { - this.snapshotListError = await error.json().then((err) => err) + this.branchSnapshotsError = await error.json().then((err) => err) } return response diff --git a/ui/packages/shared/pages/CreateBranch/utils/index.ts b/ui/packages/shared/pages/CreateBranch/utils/index.ts index ea6b9d7e467713ad2f03a30cc1249ea3a7380a75..4b5c43aadc6968ea1527e6f1d36bbceb643966c2 100644 --- a/ui/packages/shared/pages/CreateBranch/utils/index.ts +++ b/ui/packages/shared/pages/CreateBranch/utils/index.ts @@ -2,10 +2,16 @@ export const getCliCreateBranchCommand = ( branchName: string, parentBranchName: string, ) => { - return `dblab branch create ${branchName ? branchName : ``} ${ - parentBranchName !== `master` ? parentBranchName : `` - }` -} + const branchArg = branchName || ``; + const parentBranchArg = + parentBranchName && parentBranchName !== `main` + ? `--parent-branch ${parentBranchName}` + : ``; + + return `dblab branch ${branchArg} ${parentBranchArg}`.trim(); +}; + + export const getCliBranchListCommand = () => { return `dblab branch` diff --git a/ui/packages/shared/pages/CreateClone/index.tsx b/ui/packages/shared/pages/CreateClone/index.tsx index c7fdcf2c7ca2c473a73f2a2f543d4763bc57ba97..0b62c9757c096f389eeb9f2378fb100943990d3b 100644 --- a/ui/packages/shared/pages/CreateClone/index.tsx +++ b/ui/packages/shared/pages/CreateClone/index.tsx @@ -12,7 +12,6 @@ import { Select } from '@postgres.ai/shared/components/Select' import { Button } from '@postgres.ai/shared/components/Button' import { Spinner } from '@postgres.ai/shared/components/Spinner' import { ErrorStub } from '@postgres.ai/shared/components/ErrorStub' -import { compareSnapshotsDesc } from '@postgres.ai/shared/utils/snapshot' import { round } from '@postgres.ai/shared/utils/numbers' import { formatBytesIEC } from '@postgres.ai/shared/utils/units' import { SectionTitle } from '@postgres.ai/shared/components/SectionTitle' @@ -23,9 +22,11 @@ import { validatePassword, } from '@postgres.ai/shared/helpers/getEntropy' +import { Snapshot } from 'types/api/entities/snapshot' import { useCreatedStores, MainStoreApi } from './useCreatedStores' import { useForm, FormValues } from './useForm' import { getCliCloneStatus, getCliCreateCloneCommand } from './utils' +import { compareSnapshotsDesc } from '@postgres.ai/shared/utils/snapshot' import styles from './styles.module.scss' @@ -45,9 +46,9 @@ type Props = Host export const CreateClone = observer((props: Props) => { const history = useHistory() const stores = useCreatedStores(props.api) - const cloneError = stores.main.cloneError const timer = useTimer() const [branchesList, setBranchesList] = useState([]) + const [snapshots, setSnapshots] = useState([] as Snapshot[]) // Form. const onSubmit = async (values: FormValues) => { @@ -65,45 +66,66 @@ export const CreateClone = observer((props: Props) => { formik.setFieldError('dbPassword', '') - if (!isSuccess || cloneError) { + if (!isSuccess || stores.main.cloneError) { timer.pause() timer.reset() } } + const fetchBranchSnapshotsData = async (branchName: string) => { + const snapshotsRes = + (await stores.main.getBranchSnapshots(branchName)) ?? [] + setSnapshots(snapshotsRes) + formik.setFieldValue('snapshotId', snapshotsRes[0]?.id) + } + + const handleSelectBranch = async ( + e: React.ChangeEvent<{ value: string }>, + ) => { + const selectedBranch = e.target.value + formik.setFieldValue('branch', selectedBranch) + + if (props.api.getBranchSnapshots) { + await fetchBranchSnapshotsData(selectedBranch) + } + } + const formik = useForm(onSubmit) + const fetchData = async () => { + try { + await stores.main.load(props.instanceId) + + const branches = (await stores.main.getBranches()) ?? [] + const initiallySelectedBranch = branches[0]?.name + setBranchesList(branches.map((branch) => branch.name)) + formik.setFieldValue('branch', initiallySelectedBranch) + + if (props.api.getBranchSnapshots) { + await fetchBranchSnapshotsData(initiallySelectedBranch) + } else { + const allSnapshots = stores.main?.snapshots?.data ?? [] + setSnapshots(allSnapshots) + const [firstSnapshot] = allSnapshots ?? [] + formik.setFieldValue('snapshotId', firstSnapshot?.id) + } + } catch (error) { + console.error('Error fetching data:', error) + } + } + // Initial loading data. useEffect(() => { - stores.main.load(props.instanceId) - - stores.main.getBranches().then((response) => { - if (response) { - setBranchesList(response.map((branch) => branch.name)) - } - }) + fetchData() }, []) // Redirect when clone is created and stable. useEffect(() => { - if (!stores.main.clone) return - if (!stores.main.isCloneStable) return + if (!stores.main.clone || !stores.main.isCloneStable) return history.push(props.routes.clone(stores.main.clone.id)) }, [stores.main.clone, stores.main.isCloneStable]) - // Snapshots. - const sortedSnapshots = stores.main.snapshots.data - ?.slice() - .sort(compareSnapshotsDesc) - - useEffect(() => { - const [firstSnapshot] = sortedSnapshots ?? [] - if (!firstSnapshot) return - - formik.setFieldValue('snapshotId', firstSnapshot.id) - }, [Boolean(sortedSnapshots)]) - const headRendered = ( <> {/* //TODO: make global reset styles. */} @@ -114,7 +136,7 @@ export const CreateClone = observer((props: Props) => { ) // Initial loading spinner. - if (!stores.main.instance || !stores.main.snapshots.data) + if (!stores.main.instance) return ( <> {headRendered} @@ -124,23 +146,27 @@ export const CreateClone = observer((props: Props) => { ) // Instance/branches getting error. - if (stores.main.instanceError || stores.main.getBranchesError) + if ( + stores.main.instanceError || + stores.main.getBranchesError || + stores.main.getBranchSnapshotsError || + stores.main?.snapshots?.error + ) return ( <> {headRendered} ) - // Snapshots getting error. - if (stores.main.snapshots.error) - return - const isCloneUnstable = Boolean( stores.main.clone && !stores.main.isCloneStable, ) @@ -159,7 +185,7 @@ export const CreateClone = observer((props: Props) => { label="Branch" value={formik.values.branch} disabled={!branchesList || isCreatingClone} - onChange={(e) => formik.setFieldValue('branch', e.target.value)} + onChange={handleSelectBranch} error={Boolean(formik.errors.branch)} items={ branchesList?.map((snapshot) => { @@ -185,33 +211,36 @@ export const CreateClone = observer((props: Props) => { fullWidth label="Data state time *" value={formik.values.snapshotId} - disabled={!sortedSnapshots || isCreatingClone} + disabled={!snapshots || isCreatingClone} onChange={(e) => formik.setFieldValue('snapshotId', e.target.value) } error={Boolean(formik.errors.snapshotId)} items={ - sortedSnapshots?.map((snapshot, i) => { - const isLatest = i === 0 - return { - value: snapshot.id, - children: ( - <> - {snapshot.dataStateAt} - {isLatest && ( - Latest - )} - - ), - } - }) ?? [] + snapshots + .slice() + .sort(compareSnapshotsDesc) + .map((snapshot, i) => { + const isLatest = i === 0 + return { + value: snapshot.id, + children: ( + <> + {snapshot.dataStateAt} + {isLatest && ( + Latest + )} + + ), + } + }) ?? [] } />

By default latest snapshot of database is used. You can select  different snapshots if earlier database state is - needed + needed.

@@ -237,22 +266,24 @@ export const CreateClone = observer((props: Props) => { label="Database password *" type="password" value={formik.values.dbPassword} - onChange={(e) => - { formik.setFieldValue('dbPassword', e.target.value) + onChange={(e) => { + formik.setFieldValue('dbPassword', e.target.value) - if (formik.errors.dbPassword) { - formik.setFieldError('dbPassword', '') - } - }} error={Boolean(formik.errors.dbPassword)} + if (formik.errors.dbPassword) { + formik.setFieldError('dbPassword', '') + } + }} + error={Boolean(formik.errors.dbPassword)} disabled={isCreatingClone} - />

- {formik.errors.dbPassword} -

+ /> +

+ {formik.errors.dbPassword} +

@@ -271,12 +302,12 @@ export const CreateClone = observer((props: Props) => { />

- When enabled no one can delete this clone and automated deletion + When enabled no one can destroy this clone and automated deletion is also disabled.
Please be careful: abandoned clones with this checkbox enabled may cause out-of-disk-space events. Check disk space on daily basis - and delete this clone once the work is done. + and destroy this clone once the work is done.

diff --git a/ui/packages/shared/pages/CreateClone/stores/Main.ts b/ui/packages/shared/pages/CreateClone/stores/Main.ts index 29a2780d726dd398e6f125066ffe506ab65c8938..a390dd9309090b417e44320d08dc7a5ad7666864 100644 --- a/ui/packages/shared/pages/CreateClone/stores/Main.ts +++ b/ui/packages/shared/pages/CreateClone/stores/Main.ts @@ -6,6 +6,7 @@ import { GetInstance } from '@postgres.ai/shared/types/api/endpoints/getInstance import { CreateClone } from '@postgres.ai/shared/types/api/endpoints/createClone' import { GetClone } from '@postgres.ai/shared/types/api/endpoints/getClone' import { GetBranches } from '@postgres.ai/shared/types/api/endpoints/getBranches' +import { GetBranchSnapshots } from '@postgres.ai/shared/types/api/endpoints/getBranchSnapshots' import { SnapshotsStore, SnapshotsApi, @@ -22,12 +23,14 @@ export type MainStoreApi = SnapshotsApi & { createClone: CreateClone getClone: GetClone getBranches?: GetBranches + getBranchSnapshots?: GetBranchSnapshots } export class MainStore { instance: Instance | null = null instanceError: string | null = null getBranchesError: Error | null = null + getBranchSnapshotsError: Error | null = null clone: Clone | null = null cloneError: string | null = null @@ -36,13 +39,15 @@ export class MainStore { private readonly api: MainStoreApi - readonly snapshots: SnapshotsStore + readonly snapshots?: SnapshotsStore constructor(api: MainStoreApi) { makeAutoObservable(this) this.api = api - this.snapshots = new SnapshotsStore(api) + if (!api.getBranchSnapshots) { + this.snapshots = new SnapshotsStore(api) + } } get isCloneStable() { @@ -53,7 +58,7 @@ export class MainStore { load = async (instanceId: string) => { const [instance, isLoadedSnapshots] = await Promise.all([ this.api.getInstance({ instanceId }), - this.snapshots.load(instanceId), + this.snapshots?.load(instanceId) ?? true, ]) if (instance.response) this.instance = instance.response @@ -95,6 +100,16 @@ export class MainStore { return response } + getBranchSnapshots = async (branchId: string) => { + if (!this.api.getBranchSnapshots) return + const { response, error } = await this.api.getBranchSnapshots(branchId) + + if (error) + this.getBranchSnapshotsError = await error.json().then((err) => err) + + return response + } + private updateCloneUntilStable = async (args: { instanceId: string cloneId: string diff --git a/ui/packages/shared/pages/Instance/Clones/ClonesModal/index.tsx b/ui/packages/shared/pages/Instance/Clones/ClonesModal/index.tsx index 681a533e61d75e93ccba90da65cfafd4d240cbb2..283e85af89701103a5fd367536cdf3fa3cdf4690 100644 --- a/ui/packages/shared/pages/Instance/Clones/ClonesModal/index.tsx +++ b/ui/packages/shared/pages/Instance/Clones/ClonesModal/index.tsx @@ -64,7 +64,7 @@ export const ClonesModal = observer(() => { !snapshotId || snapshotId === clone.snapshot?.id return isMatchedByPool && isMatchedBySnapshot })} - emptyStubText="No clones found" + emptyStubText="No clones found." /> ) diff --git a/ui/packages/shared/pages/Instance/Clones/index.tsx b/ui/packages/shared/pages/Instance/Clones/index.tsx index 9d8cc1dc5e234e2e5c497971bf427c6f03d10ab7..35e38d82f46c16d244a20aab64a5a5be2625eca7 100644 --- a/ui/packages/shared/pages/Instance/Clones/index.tsx +++ b/ui/packages/shared/pages/Instance/Clones/index.tsx @@ -126,7 +126,7 @@ export const Clones = observer((props: ClonesProps) => { : instance.state.cloning.clones } isDisabled={stores.main.isDisabledInstance} - emptyStubText="This instance has no active clones" + emptyStubText="This instance has no active clones." /> {showListSizeButton && !onlyRenderList && ( diff --git a/ui/packages/shared/pages/Instance/Snapshots/components/SnapshotsTable/index.tsx b/ui/packages/shared/pages/Instance/Snapshots/components/SnapshotsTable/index.tsx index 1f5e782d6fada821b91c6f8bcf7a9d35142ee3ff..29b875add43d3560f8c36f1759b02280593eecb5 100644 --- a/ui/packages/shared/pages/Instance/Snapshots/components/SnapshotsTable/index.tsx +++ b/ui/packages/shared/pages/Instance/Snapshots/components/SnapshotsTable/index.tsx @@ -5,227 +5,226 @@ *-------------------------------------------------------------------------- */ - import React from 'react' - import cn from 'classnames' - import { observer } from 'mobx-react-lite' - import { makeStyles } from '@material-ui/core' - import { formatDistanceToNowStrict } from 'date-fns' - import copy from 'copy-to-clipboard' - import { useHistory } from 'react-router-dom' - - import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer' - import { generateSnapshotPageId } from '@postgres.ai/shared/pages/Instance/Snapshots/utils' - import { DestroySnapshotModal } from '@postgres.ai/shared/pages/Snapshots/Snapshot/DestorySnapshotModal' - import { useStores } from '@postgres.ai/shared/pages/Instance/context' - import { ArrowDropDownIcon } from '@postgres.ai/shared/icons/ArrowDropDown' - import { formatBytesIEC } from '@postgres.ai/shared/utils/units' - import { isSameDayUTC, isValidDate } from '@postgres.ai/shared/utils/date' - import { - Table, - TableHead, - TableRow, - TableBody, - TableHeaderCell, - TableBodyCell, - TableBodyCellMenu, - } from '@postgres.ai/shared/components/Table' - - const useStyles = makeStyles( - { - cellContentCentered: { - display: 'flex', - alignItems: 'center', - }, - pointerCursor: { - cursor: 'pointer', - }, - sortIcon: { - marginLeft: '8px', - width: '10px', - cursor: 'pointer', - transition: 'transform 0.15s ease-in-out', - }, - - sortIconUp: { - transform: 'rotate(180deg)', - }, - - hideSortIcon: { - opacity: 0, - }, - - verticalCentered: { - display: 'flex', - alignItems: 'center', - }, - }, - { index: 1 }, - ) - - export const SnapshotsTable = observer(() => { - const history = useHistory() - const classes = useStyles() - const stores = useStores() - const { snapshots } = stores.main - - const [snapshotModal, setSnapshotModal] = React.useState({ - isOpen: false, - snapshotId: '', - }) - - const filteredSnapshots = snapshots?.data?.filter((snapshot) => { - const isMatchedByDate = - !stores.snapshotsModal.date || - isSameDayUTC(snapshot.dataStateAtDate, stores.snapshotsModal.date) - - const isMatchedByPool = - !stores.snapshotsModal.pool || - snapshot.pool === stores.snapshotsModal.pool - - return isMatchedByDate && isMatchedByPool - }) - - const [state, setState] = React.useState({ - sortByCreatedDate: 'desc', - snapshots: filteredSnapshots ?? [], - }) - - const handleSortByCreatedDate = () => { - const sortByCreatedDate = - state.sortByCreatedDate === 'desc' ? 'asc' : 'desc' - - const sortedSnapshots = [...state.snapshots].sort((a, b) => { - if (sortByCreatedDate === 'asc') { - return ( - new Date(a.createdAtDate).getTime() - - new Date(b.createdAtDate).getTime() - ) - } else { - return ( - new Date(b.createdAtDate).getTime() - - new Date(a.createdAtDate).getTime() - ) - } - }) - - setState({ - ...state, - sortByCreatedDate, - snapshots: sortedSnapshots, - }) - } - - if (!snapshots.data) return null - - return ( - - - - - - Data state time - -
- Created - -
-
- Pool - Number of clones - Logical Size - Physical Size -
-
- - {state.snapshots?.map((snapshot) => { - const snapshotPageId = generateSnapshotPageId(snapshot.id) - return ( - - snapshotPageId && - history.push(`/instance/snapshots/${snapshotPageId}`) - } - className={classes.pointerCursor} - > - copy(snapshot.id), - }, - { - name: 'Show related clones', - onClick: () => - stores.clonesModal.openModal({ - snapshotId: snapshot.id, - }), - }, - { - name: 'Delete snapshot', - onClick: () => - setSnapshotModal({ - isOpen: true, - snapshotId: snapshot.id, - }), - }, - ]} - /> - - {snapshot.dataStateAt} ( - {isValidDate(snapshot.dataStateAtDate) - ? formatDistanceToNowStrict(snapshot.dataStateAtDate, { - addSuffix: true, - }) - : '-'} - ) - - - {snapshot.createdAt} ( - {isValidDate(snapshot.createdAtDate) - ? formatDistanceToNowStrict(snapshot.createdAtDate, { - addSuffix: true, - }) - : '-'} - ) - - {snapshot.pool ?? '-'} - {snapshot.numClones ?? '-'} - - {snapshot.logicalSize - ? formatBytesIEC(snapshot.logicalSize) - : '-'} - - - {snapshot.physicalSize - ? formatBytesIEC(snapshot.physicalSize) - : '-'} - - - ) - })} - - {snapshotModal.isOpen && snapshotModal.snapshotId && ( - setSnapshotModal({ isOpen: false, snapshotId: '' })} - snapshotId={snapshotModal.snapshotId} - afterSubmitClick={() => - stores.main?.reload(stores.main.instance?.id ?? '') - } - /> - )} -
-
- ) - }) - \ No newline at end of file +import React from 'react' +import cn from 'classnames' +import { observer } from 'mobx-react-lite' +import { makeStyles } from '@material-ui/core' +import { formatDistanceToNowStrict } from 'date-fns' +import copy from 'copy-to-clipboard' +import { useHistory } from 'react-router-dom' + +import { HorizontalScrollContainer } from '@postgres.ai/shared/components/HorizontalScrollContainer' +import { generateSnapshotPageId } from '@postgres.ai/shared/pages/Instance/Snapshots/utils' +import { DestroySnapshotModal } from '@postgres.ai/shared/pages/Snapshots/Snapshot/DestorySnapshotModal' +import { useStores } from '@postgres.ai/shared/pages/Instance/context' +import { ArrowDropDownIcon } from '@postgres.ai/shared/icons/ArrowDropDown' +import { formatBytesIEC } from '@postgres.ai/shared/utils/units' +import { isSameDayUTC, isValidDate } from '@postgres.ai/shared/utils/date' +import { + Table, + TableHead, + TableRow, + TableBody, + TableHeaderCell, + TableBodyCell, + TableBodyCellMenu, +} from '@postgres.ai/shared/components/Table' + +const useStyles = makeStyles( + { + cellContentCentered: { + display: 'flex', + alignItems: 'center', + }, + pointerCursor: { + cursor: 'pointer', + }, + sortIcon: { + marginLeft: '8px', + width: '10px', + cursor: 'pointer', + transition: 'transform 0.15s ease-in-out', + }, + + sortIconUp: { + transform: 'rotate(180deg)', + }, + + hideSortIcon: { + opacity: 0, + }, + + verticalCentered: { + display: 'flex', + alignItems: 'center', + }, + }, + { index: 1 }, +) + +export const SnapshotsTable = observer(() => { + const history = useHistory() + const classes = useStyles() + const stores = useStores() + const { snapshots } = stores.main + + const [snapshotModal, setSnapshotModal] = React.useState({ + isOpen: false, + snapshotId: '', + }) + + const filteredSnapshots = snapshots?.data?.filter((snapshot) => { + const isMatchedByDate = + !stores.snapshotsModal.date || + isSameDayUTC(snapshot.dataStateAtDate, stores.snapshotsModal.date) + + const isMatchedByPool = + !stores.snapshotsModal.pool || + snapshot.pool === stores.snapshotsModal.pool + + return isMatchedByDate && isMatchedByPool + }) + + const [state, setState] = React.useState({ + sortByCreatedDate: 'desc', + snapshots: filteredSnapshots ?? [], + }) + + const handleSortByCreatedDate = () => { + const sortByCreatedDate = + state.sortByCreatedDate === 'desc' ? 'asc' : 'desc' + + const sortedSnapshots = [...state.snapshots].sort((a, b) => { + if (sortByCreatedDate === 'asc') { + return ( + new Date(a.createdAtDate).getTime() - + new Date(b.createdAtDate).getTime() + ) + } else { + return ( + new Date(b.createdAtDate).getTime() - + new Date(a.createdAtDate).getTime() + ) + } + }) + + setState({ + ...state, + sortByCreatedDate, + snapshots: sortedSnapshots, + }) + } + + if (!snapshots.data) return null + + return ( + + + + + + Data state time + +
+ Created + +
+
+ Pool + Number of clones + Logical Size + Physical Size +
+
+ + {state.snapshots?.map((snapshot) => { + const snapshotPageId = generateSnapshotPageId(snapshot.id) + return ( + + snapshotPageId && + history.push(`/instance/snapshots/${snapshotPageId}`) + } + className={classes.pointerCursor} + > + copy(snapshot.id), + }, + { + name: 'Show related clones', + onClick: () => + stores.clonesModal.openModal({ + snapshotId: snapshot.id, + }), + }, + { + name: 'Destroy snapshot', + onClick: () => + setSnapshotModal({ + isOpen: true, + snapshotId: snapshot.id, + }), + }, + ]} + /> + + {snapshot.dataStateAt} ( + {isValidDate(snapshot.dataStateAtDate) + ? formatDistanceToNowStrict(snapshot.dataStateAtDate, { + addSuffix: true, + }) + : '-'} + ) + + + {snapshot.createdAt} ( + {isValidDate(snapshot.createdAtDate) + ? formatDistanceToNowStrict(snapshot.createdAtDate, { + addSuffix: true, + }) + : '-'} + ) + + {snapshot.pool ?? '-'} + {snapshot.numClones ?? '-'} + + {snapshot.logicalSize + ? formatBytesIEC(snapshot.logicalSize) + : '-'} + + + {snapshot.physicalSize + ? formatBytesIEC(snapshot.physicalSize) + : '-'} + + + ) + })} + + {snapshotModal.isOpen && snapshotModal.snapshotId && ( + setSnapshotModal({ isOpen: false, snapshotId: '' })} + snapshotId={snapshotModal.snapshotId} + afterSubmitClick={() => + stores.main?.reload(stores.main.instance?.id ?? '') + } + /> + )} +
+
+ ) +}) diff --git a/ui/packages/shared/pages/Instance/stores/Main.ts b/ui/packages/shared/pages/Instance/stores/Main.ts index 3f7104482e5bd01f63503cee54726006f0a7355d..d7c61f29b321c7396720db3a0d081d445788bfde 100644 --- a/ui/packages/shared/pages/Instance/stores/Main.ts +++ b/ui/packages/shared/pages/Instance/stores/Main.ts @@ -72,7 +72,6 @@ export class MainStore { getFullConfigError: string | null = null getBranchesError: Error | null = null snapshotListError: string | null = null - deleteBranchError: Error | null = null seImagesError: string | undefined | null = null unstableClones = new Set() @@ -369,19 +368,14 @@ export class MainStore { return response } - deleteBranch = async (branchName: string) => { - if (!branchName || !this.api.deleteBranch) return +deleteBranch = async (branchName: string) => { + if (!branchName || !this.api.deleteBranch) return - this.deleteBranchError = null + const { response, error } = await this.api.deleteBranch(branchName) - const { response, error } = await this.api.deleteBranch(branchName) - if (error) { - this.deleteBranchError = await error.json().then((err) => err) - } - - return response - } + return { response, error } +} getSnapshotList = async (branchName: string) => { if (!this.api.getSnapshotList) return diff --git a/ui/packages/shared/pages/Snapshots/Snapshot/DestorySnapshotModal/index.tsx b/ui/packages/shared/pages/Snapshots/Snapshot/DestorySnapshotModal/index.tsx index 3a259e40715e6ebac48028f5b85d63855b3e24c2..a9ab15e1d5a862db8fec30bf84c10140905f96df 100644 --- a/ui/packages/shared/pages/Snapshots/Snapshot/DestorySnapshotModal/index.tsx +++ b/ui/packages/shared/pages/Snapshots/Snapshot/DestorySnapshotModal/index.tsx @@ -51,7 +51,7 @@ export const DestroySnapshotModal = ({ const handleClickDestroy = () => { destroySnapshot(snapshotId).then((res) => { - if (res?.error?.message) { + if (res?.error?.message) { setDeleteError(res.error.message) } else { afterSubmitClick() @@ -60,12 +60,12 @@ export const DestroySnapshotModal = ({ }) } - return ( Are you sure you want to destroy snapshot{' '} - {snapshotId}? + {snapshotId}? This action cannot be + undone. {deleteError &&

{deleteError}

} { className={classes.marginTop} tag="h2" level={2} - text={'Delete snapshot using CLI'} + text={'Destroy snapshot using CLI'} />

- You can delete this snapshot using CLI. To do this, run the command + You can destroy this snapshot using CLI. To do this, run the command below:

- + Promise<{ response: Response | null; error: Response | null }> +) => Promise<{ response: Response | null; error: Error | null }> diff --git a/ui/packages/shared/types/api/endpoints/destroySnapshot.ts b/ui/packages/shared/types/api/endpoints/destroySnapshot.ts index d38c93657c7116b69933b0e771e2984b47420a80..0d0f96e11a7a917261b23f5dcff9301c696c7c9e 100644 --- a/ui/packages/shared/types/api/endpoints/destroySnapshot.ts +++ b/ui/packages/shared/types/api/endpoints/destroySnapshot.ts @@ -1,4 +1,4 @@ export type DestroySnapshot = (snapshotId: string) => Promise<{ - response: true | null + response: boolean | null error: Response | null }> diff --git a/ui/packages/shared/types/api/endpoints/getBranchSnapshots.ts b/ui/packages/shared/types/api/endpoints/getBranchSnapshots.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e6530e35d2ae86981f80cab47da64d721ac5a42 --- /dev/null +++ b/ui/packages/shared/types/api/endpoints/getBranchSnapshots.ts @@ -0,0 +1,5 @@ +import { Snapshot } from '@postgres.ai/shared/types/api/entities/snapshot' + +export type GetBranchSnapshots = ( + snapshotId: string, +) => Promise<{ response: Snapshot[] | null; error: Response | null }> diff --git a/ui/packages/shared/types/api/endpoints/getBranches.ts b/ui/packages/shared/types/api/endpoints/getBranches.ts index d5825686b106878644aea2f7ff28c4236da21df6..b4ac47d0c1f9b1463176064a80d7d2a2ae5009f4 100644 --- a/ui/packages/shared/types/api/endpoints/getBranches.ts +++ b/ui/packages/shared/types/api/endpoints/getBranches.ts @@ -1,11 +1,19 @@ -export interface GetBranchesResponseType { +import { formatDateToISO } from '@postgres.ai/shared/utils/date' + +export interface Branch { name: string parent: string dataStateAt: string snapshotID: string } +export const formatBranchesDto = (dto: Branch[]) => + dto.map((item) => ({ + ...item, + dataStateAt: formatDateToISO(item.dataStateAt), + })) + export type GetBranches = () => Promise<{ - response: GetBranchesResponseType[] | null + response: Branch[] | null error: Response | null }> diff --git a/ui/packages/shared/types/api/endpoints/getSnapshotList.ts b/ui/packages/shared/types/api/endpoints/getSnapshotList.ts index 0c8f277e7a2bf0c1d755cd38d93f4306e6007d2c..b5716ff07e565c3c66492d85302c88b96e6120a9 100644 --- a/ui/packages/shared/types/api/endpoints/getSnapshotList.ts +++ b/ui/packages/shared/types/api/endpoints/getSnapshotList.ts @@ -1,4 +1,4 @@ -export interface GetSnapshotListResponseType { +export interface SnapshotList { branch: string[] id: string dataStateAt: string @@ -6,6 +6,6 @@ export interface GetSnapshotListResponseType { } export type GetSnapshotList = (branchName: string) => Promise<{ - response: GetSnapshotListResponseType[] | null + response: SnapshotList[] | null error: Response | null }> diff --git a/ui/packages/shared/types/api/entities/branchSnapshots.ts b/ui/packages/shared/types/api/entities/branchSnapshots.ts new file mode 100644 index 0000000000000000000000000000000000000000..6727522d15a6284e7e7482f6b6ac8dd09a77517b --- /dev/null +++ b/ui/packages/shared/types/api/entities/branchSnapshots.ts @@ -0,0 +1,10 @@ +import { parseDate } from 'utils/date' +import { SnapshotDto } from './snapshot' + +export const formatBranchSnapshotDto = (dto: SnapshotDto[]) => + dto.map((item) => ({ + ...item, + numClones: item.numClones.toString(), + createdAtDate: parseDate(item.createdAt), + dataStateAtDate: parseDate(item.dataStateAt), + })) diff --git a/ui/packages/shared/types/api/entities/snapshot.ts b/ui/packages/shared/types/api/entities/snapshot.ts index 97c8e41e8232589c10141c01bba1b386ec6b1dda..46768165bd4a6726f12a9261a5b0f0bdf4e6707e 100644 --- a/ui/packages/shared/types/api/entities/snapshot.ts +++ b/ui/packages/shared/types/api/entities/snapshot.ts @@ -1,7 +1,7 @@ import { parseDate } from '@postgres.ai/shared/utils/date' export type SnapshotDto = { - numClones: string + numClones: string | number createdAt: string dataStateAt: string id: string diff --git a/ui/packages/shared/utils/date.ts b/ui/packages/shared/utils/date.ts index 4b6c6b9222887bda1028f69ad37a93a137d10d1b..0b024c58d34798ca5c25e26902111a72fb79b7ee 100644 --- a/ui/packages/shared/utils/date.ts +++ b/ui/packages/shared/utils/date.ts @@ -21,6 +21,11 @@ import { formatDistanceToNowStrict, } from 'date-fns' +export const formatDateToISO = (dateString: string) => { + const parsedDate = parse(dateString, 'yyyyMMddHHmmss', new Date()) + return format(parsedDate, "yyyy-MM-dd'T'HH:mm:ssXXX") +} + // parseDate parses date of both format: '2006-01-02 15:04:05 UTC' and `2006-01-02T15:04:05Z` (RFC3339). export const parseDate = (dateStr: string) => parse( @@ -88,4 +93,4 @@ export const formatDateStd = ( export const isValidDate = (dateObject: Date) => { return new Date(dateObject).toString() !== 'Invalid Date' -} \ No newline at end of file +} diff --git a/ui/packages/shared/utils/snapshot.ts b/ui/packages/shared/utils/snapshot.ts index 6e5c5d905e749b8cfc3068b69b949d2c3162c41d..255e76bab0a76e44cfd3258aa05114acfd93fea8 100644 --- a/ui/packages/shared/utils/snapshot.ts +++ b/ui/packages/shared/utils/snapshot.ts @@ -1,4 +1,8 @@ -import { Snapshot } from '@postgres.ai/shared/types/api/entities/snapshot' - -export const compareSnapshotsDesc = (a: Snapshot, b: Snapshot) => - b.dataStateAtDate.getTime() - a.dataStateAtDate.getTime() +export const compareSnapshotsDesc = ( + a: { dataStateAtDate: Date }, + b: { dataStateAtDate: Date }, +): number => { + const { dataStateAtDate: dateA } = a + const { dataStateAtDate: dateB } = b + return dateB.getTime() - dateA.getTime() +}