Skip to content

Commit

Permalink
feat: [UIE-8009] - DBaaS enhancements Backups (#10961)
Browse files Browse the repository at this point in the history
* feat: [UIE-8009] - DBaaS enhancements Backups

* Added changeset: DBaaS V2 enhancements to the Backups

* feat: [UIE-8009] - Review fix: refactoring

* feat: [UIE-8009] - Review fix: replace Time Picker, refactoring
  • Loading branch information
mpolotsk-akamai authored Sep 19, 2024
1 parent 5f08477 commit 109bac6
Show file tree
Hide file tree
Showing 20 changed files with 626 additions and 35 deletions.
23 changes: 21 additions & 2 deletions packages/api-v4/src/databases/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,11 @@ export const getDatabaseBackup = (
);

/**
* restoreWithBackup
* legacyRestoreWithBackup
*
* Fully restore a backup to the cluster
*/
export const restoreWithBackup = (
export const legacyRestoreWithBackup = (
engine: Engine,
databaseID: number,
backupID: number
Expand All @@ -243,6 +243,25 @@ export const restoreWithBackup = (
setMethod('POST')
);

/**
* newRestoreWithBackup for the New Database
*
* Fully restore a backup to the cluster
*/
export const newRestoreWithBackup = (
engine: Engine,
label: string,
fork: {
source: number;
restore_time?: string;
}
) =>
Request<{}>(
setURL(`${API_ROOT}/databases/${encodeURIComponent(engine)}/instances`),
setMethod('POST'),
setData({ fork, label })
);

/**
* getDatabaseCredentials
*
Expand Down
1 change: 1 addition & 0 deletions packages/api-v4/src/databases/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export interface BaseDatabase {
*/
members: Record<string, MemberType>;
platform?: string;
oldest_restore_time?: string;
}

export interface MySQLDatabase extends BaseDatabase {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

DBaaS V2 enhancements to the Backups ([#10961](https://github.com/linode/manager/pull/10961))
5 changes: 3 additions & 2 deletions packages/manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,18 @@
"@hookform/resolvers": "2.9.11",
"@linode/api-v4": "*",
"@linode/design-language-system": "^2.6.1",
"@linode/validation": "*",
"@linode/search": "*",
"@linode/validation": "*",
"@lukemorales/query-key-factory": "^1.3.4",
"@mui/icons-material": "^5.14.7",
"@mui/material": "^5.14.7",
"@mui/x-date-pickers": "^7.12.0",
"@paypal/react-paypal-js": "^7.8.3",
"@reach/tabs": "^0.10.5",
"@sentry/react": "^7.57.0",
"@tanstack/react-query": "5.51.24",
"@tanstack/react-query-devtools": "5.51.24",
"@xterm/xterm": "^5.5.0",
"algoliasearch": "^4.14.3",
"axios": "~1.7.4",
"braintree-web": "^3.92.2",
Expand Down Expand Up @@ -79,7 +81,6 @@
"tss-react": "^4.8.2",
"typescript-fsa": "^3.0.0",
"typescript-fsa-reducers": "^1.2.0",
"@xterm/xterm": "^5.5.0",
"yup": "^0.32.9",
"zxcvbn": "^4.4.2"
},
Expand Down
8 changes: 8 additions & 0 deletions packages/manager/src/components/Dialog/Dialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ describe('Dialog', () => {
expect(getByText('Child items can go here!')).toBeInTheDocument();
});

it('should render a Dialog with subtitle if provided', () => {
const { getByText } = renderWithTheme(
<Dialog {...defaultArgs} open={true} subtitle="This is a subtitle" />
);

expect(getByText('This is a subtitle')).toBeInTheDocument();
});

it('should call onClose when the Dialog close button is clicked', () => {
const { getByRole } = renderWithTheme(
<Dialog {...defaultArgs} open={true} />
Expand Down
3 changes: 3 additions & 0 deletions packages/manager/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface DialogProps extends _DialogProps {
className?: string;
error?: string;
fullHeight?: boolean;
subtitle?: string;
title: string;
titleBottomBorder?: boolean;
}
Expand Down Expand Up @@ -49,6 +50,7 @@ export const Dialog = (props: DialogProps) => {
fullWidth,
maxWidth = 'md',
onClose,
subtitle,
title,
titleBottomBorder,
...rest
Expand Down Expand Up @@ -78,6 +80,7 @@ export const Dialog = (props: DialogProps) => {
<DialogTitle
id={titleID}
onClose={() => onClose && onClose({}, 'backdropClick')}
subtitle={subtitle}
title={title}
/>
{titleBottomBorder && <StyledHr />}
Expand Down
10 changes: 7 additions & 3 deletions packages/manager/src/components/DialogTitle/DialogTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import Close from '@mui/icons-material/Close';
import { Box } from 'src/components/Box';
import { Typography } from '@mui/material';
import _DialogTitle from '@mui/material/DialogTitle';
import { SxProps } from '@mui/system';
import * as React from 'react';

import { Box } from 'src/components/Box';
import { IconButton } from 'src/components/IconButton';

import type { SxProps } from '@mui/system';

interface DialogTitleProps {
className?: string;
id?: string;
onClose?: () => void;
subtitle?: string;
sx?: SxProps;
title: string;
}

const DialogTitle = (props: DialogTitleProps) => {
const ref = React.useRef<HTMLDivElement>(null);
const { className, id, onClose, sx, title } = props;
const { className, id, onClose, subtitle, sx, title } = props;

React.useEffect(() => {
if (ref.current === null) {
Expand Down Expand Up @@ -63,6 +66,7 @@ const DialogTitle = (props: DialogTitleProps) => {
</IconButton>
)}
</Box>
{subtitle && <Typography>{subtitle}</Typography>}
</_DialogTitle>
);
};
Expand Down
8 changes: 7 additions & 1 deletion packages/manager/src/factories/databases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export const databaseFactory = Factory.Sync.makeFactory<Database>({
members: {
'2.2.2.2': 'primary',
},
oldest_restore_time: '2024-09-15T17:15:12',
platform: pickRandom(['rdbms-legacy', 'rdbms-default']),
port: 3306,
region: 'us-east',
Expand All @@ -242,7 +243,12 @@ export const databaseFactory = Factory.Sync.makeFactory<Database>({
});

export const databaseBackupFactory = Factory.Sync.makeFactory<DatabaseBackup>({
created: Factory.each(() => randomDate().toISOString()),
created: Factory.each(() =>
randomDate(
new Date(Date.now() - 10 * 24 * 60 * 60 * 1000),
new Date()
).toISOString()
),
id: Factory.each((i) => i),
label: Factory.each(() => `backup-${v4()}`),
type: pickRandom(['snapshot', 'auto']),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { styled } from '@mui/material/styles';
import { DateCalendar, TimePicker } from '@mui/x-date-pickers';

import { Box } from 'src/components/Box';
import { Typography } from 'src/components/Typography';

export const StyledTimePicker = styled(TimePicker)(() => ({
'.MuiInputAdornment-root': { marginRight: '0' },
'.MuiInputBase-input': { padding: '8px 0 8px 12px' },
'.MuiInputBase-root': { borderRadius: '0', padding: '0px' },

'button.MuiButtonBase-root': {
marginRight: '0',
padding: '8px',
},
height: '34px',
marginTop: '8px',
width: '120px',
}));

export const StyledDateCalendar = styled(DateCalendar, {
label: 'StyledDateCalendar',
})(({ theme }) => ({
'.MuiButtonBase-root.MuiPickersDay-root.Mui-disabled': {
color: theme.color.grey3,
},
'.MuiPickersArrowSwitcher-spacer': { width: '15px' },
'.MuiPickersCalendarHeader-labelContainer': {
fontSize: '0.95rem',
},
'.MuiPickersCalendarHeader-root': {
marginTop: '0',
paddingLeft: '17px',
paddingRight: '3px',
},
'.MuiPickersCalendarHeader-switchViewIcon': {
fontSize: '28px',
},
'.MuiPickersDay-root': {
fontSize: '0.875rem',
height: '32px',
width: '32px',
},
'.MuiSvgIcon-root': {
fontSize: '22px',
},
'.MuiTypography-root': {
fontSize: '0.875rem',
height: '32px',
width: '32px',
},
'.MuiYearCalendar-root': {
width: '260px',
},
marginLeft: '0px',
width: '260px',
}));

export const StyledBox = styled(Box)(({ theme }) => ({
'& h6': {
fontSize: '0.875rem',
},
'& span': {
marginBottom: '5px',
marginTop: '7px',
},
alignItems: 'flex-start',

border: '1px solid #F4F4F4',
color: theme.name === 'light' ? '#555555' : theme.color.headline,
display: 'flex',
flexDirection: 'column',
height: '100%',
padding: '8px 15px',
background: theme.name === 'light' ? '#FBFBFB' : theme.color.grey2,
}));

export const StyledTypography = styled(Typography)(() => ({
lineHeight: '20px',
marginTop: '4px',
}));
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import DatabaseBackups from './DatabaseBackups';

describe('Database Backups', () => {
it('should render a list of backups after loading', async () => {
const mockDatabase = databaseFactory.build({
platform: 'rdbms-legacy',
});
const backups = databaseBackupFactory.buildList(7);

// Mock the Database because the Backups Details page requires it to be loaded
Expand All @@ -22,7 +25,7 @@ describe('Database Backups', () => {
return HttpResponse.json(profileFactory.build({ timezone: 'utc' }));
}),
http.get('*/databases/:engine/instances/:id', () => {
return HttpResponse.json(databaseFactory.build());
return HttpResponse.json(mockDatabase);
}),
http.get('*/databases/:engine/instances/:id/backups', () => {
return HttpResponse.json(makeResourcePage(backups));
Expand All @@ -41,10 +44,13 @@ describe('Database Backups', () => {
});

it('should render an empty state if there are no backups', async () => {
const mockDatabase = databaseFactory.build({
platform: 'rdbms-legacy',
});
// Mock the Database because the Backups Details page requires it to be loaded
server.use(
http.get('*/databases/:engine/instances/:id', () => {
return HttpResponse.json(databaseFactory.build());
return HttpResponse.json(mockDatabase);
})
);

Expand All @@ -61,14 +67,17 @@ describe('Database Backups', () => {
});

it('should disable the restore button if disabled = true', async () => {
const mockDatabase = databaseFactory.build({
platform: 'rdbms-legacy',
});
const backups = databaseBackupFactory.buildList(7);

server.use(
http.get('*/profile', () => {
return HttpResponse.json(profileFactory.build({ timezone: 'utc' }));
}),
http.get('*/databases/:engine/instances/:id', () => {
return HttpResponse.json(databaseFactory.build());
return HttpResponse.json(mockDatabase);
}),
http.get('*/databases/:engine/instances/:id/backups', () => {
return HttpResponse.json(makeResourcePage(backups));
Expand All @@ -87,14 +96,17 @@ describe('Database Backups', () => {
});

it('should enable the restore button if disabled = false', async () => {
const mockDatabase = databaseFactory.build({
platform: 'rdbms-legacy',
});
const backups = databaseBackupFactory.buildList(7);

server.use(
http.get('*/profile', () => {
return HttpResponse.json(profileFactory.build({ timezone: 'utc' }));
}),
http.get('*/databases/:engine/instances/:id', () => {
return HttpResponse.json(databaseFactory.build());
return HttpResponse.json(mockDatabase);
}),
http.get('*/databases/:engine/instances/:id/backups', () => {
return HttpResponse.json(makeResourcePage(backups));
Expand All @@ -111,4 +123,29 @@ describe('Database Backups', () => {
expect(button).toBeEnabled();
});
});

it('should render a time picker when it is a new database', async () => {
const mockDatabase = databaseFactory.build({
platform: 'rdbms-default',
});
const backups = databaseBackupFactory.buildList(7);

server.use(
http.get('*/profile', () => {
return HttpResponse.json(profileFactory.build({ timezone: 'utc' }));
}),
http.get('*/databases/:engine/instances/:id', () => {
return HttpResponse.json(mockDatabase);
}),
http.get('*/databases/:engine/instances/:id/backups', () => {
return HttpResponse.json(makeResourcePage(backups));
})
);

const { findByText } = renderWithTheme(
<DatabaseBackups disabled={false} />
);
const timePickerLabel = await findByText('Time (UTC)');
expect(timePickerLabel).toBeInTheDocument();
});
});
Loading

0 comments on commit 109bac6

Please sign in to comment.