diff --git a/packages/api-v4/.changeset/pr-11039-added-1727885059968.md b/packages/api-v4/.changeset/pr-11039-added-1727885059968.md new file mode 100644 index 00000000000..edb8b314aed --- /dev/null +++ b/packages/api-v4/.changeset/pr-11039-added-1727885059968.md @@ -0,0 +1,5 @@ +--- +'@linode/api-v4': Added +--- + +DBaaS 2.0: Add allow_list to the DatabaseInstance ([#11039](https://github.com/linode/manager/pull/11039)) diff --git a/packages/api-v4/src/databases/types.ts b/packages/api-v4/src/databases/types.ts index feb7987fde2..f4852052f90 100644 --- a/packages/api-v4/src/databases/types.ts +++ b/packages/api-v4/src/databases/types.ts @@ -88,6 +88,7 @@ export interface DatabaseInstance { */ members: Record; platform?: string; + allow_list: string[]; } export type ClusterSize = 1 | 2 | 3; diff --git a/packages/manager/.changeset/pr-11039-upcoming-features-1727885138241.md b/packages/manager/.changeset/pr-11039-upcoming-features-1727885138241.md new file mode 100644 index 00000000000..7a7f1ea1164 --- /dev/null +++ b/packages/manager/.changeset/pr-11039-upcoming-features-1727885138241.md @@ -0,0 +1,5 @@ +--- +'@linode/manager': Upcoming Features +--- + +Added Action Menu Column to the Databases Table and update Database Logo ([#11039](https://github.com/linode/manager/pull/11039)) diff --git a/packages/manager/src/factories/databases.ts b/packages/manager/src/factories/databases.ts index 33d95cca170..fe7f5f1ecc0 100644 --- a/packages/manager/src/factories/databases.ts +++ b/packages/manager/src/factories/databases.ts @@ -173,6 +173,7 @@ const adb10 = (i: number) => i % 2 === 0; export const databaseInstanceFactory = Factory.Sync.makeFactory( { + allow_list: [], cluster_size: Factory.each((i) => adb10(i) ? ([1, 3][i % 2] as ClusterSize) diff --git a/packages/manager/src/features/Databases/DatabaseDetail/AccessControls.tsx b/packages/manager/src/features/Databases/DatabaseDetail/AccessControls.tsx index 1e2a9070474..a1b7f157ec9 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/AccessControls.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/AccessControls.tsx @@ -12,13 +12,11 @@ import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { Typography } from 'src/components/Typography'; import { useDatabaseMutation } from 'src/queries/databases/databases'; -import { stringToExtendedIP } from 'src/utilities/ipUtils'; import AddAccessControlDrawer from './AddAccessControlDrawer'; import type { APIError, Database } from '@linode/api-v4'; import type { Theme } from '@mui/material/styles'; -import type { ExtendedIP } from 'src/utilities/ipUtils'; const useStyles = makeStyles()((theme: Theme) => ({ addAccessControlBtn: { @@ -86,11 +84,7 @@ interface Props { } export const AccessControls = (props: Props) => { - const { - database: { allow_list: allowList, engine, id }, - description, - disabled, - } = props; + const { database, description, disabled } = props; const { classes } = useStyles(); @@ -107,22 +101,10 @@ export const AccessControls = (props: Props) => { setAddAccessControlDrawerOpen, ] = React.useState(false); - const [extendedIPs, setExtendedIPs] = React.useState([]); - const { isPending: databaseUpdating, mutateAsync: updateDatabase, - } = useDatabaseMutation(engine, id); - - React.useEffect(() => { - if (allowList.length > 0) { - const allowListExtended = allowList.map(stringToExtendedIP); - - setExtendedIPs(allowListExtended); - } else { - setExtendedIPs([]); - } - }, [allowList]); + } = useDatabaseMutation(database.engine, database.id); const handleClickRemove = (accessControl: string) => { setError(undefined); @@ -136,7 +118,7 @@ export const AccessControls = (props: Props) => { const handleRemoveIPAddress = () => { updateDatabase({ - allow_list: allowList.filter( + allow_list: database.allow_list.filter( (ipAddress) => ipAddress !== accessControlToBeRemoved ), }) @@ -206,7 +188,7 @@ export const AccessControls = (props: Props) => { Manage Access Controls - {ipTable(allowList)} + {ipTable(database.allow_list)} { setAddAccessControlDrawerOpen(false)} open={addAccessControlDrawerOpen} - updateDatabase={updateDatabase} /> ); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/AddAccessControlDrawer.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/AddAccessControlDrawer.test.tsx index 23a5a3a5399..c41e81f6cc2 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/AddAccessControlDrawer.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/AddAccessControlDrawer.test.tsx @@ -3,9 +3,9 @@ import * as React from 'react'; import { databaseFactory } from 'src/factories'; import { IPv4List } from 'src/factories/databases'; -import { stringToExtendedIP } from 'src/utilities/ipUtils'; import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; +import { DatabaseInstance } from '@linode/api-v4'; import AccessControls from './AccessControls'; import AddAccessControlDrawer from './AddAccessControlDrawer'; @@ -27,13 +27,13 @@ describe('Add Access Controls drawer', () => { it('Should open with a full list of current inbound sources that are allow listed', async () => { const IPv4ListWithMasks = IPv4List.map((ip) => `${ip}/32`); + const db = { + id: 123, + engine: 'postgresql', + allow_list: IPv4ListWithMasks, + } as DatabaseInstance; const { getAllByTestId } = renderWithTheme( - null} - open={true} - updateDatabase={() => null} - /> + null} open={true} /> ); expect(getAllByTestId('domain-transfer-input')).toHaveLength( @@ -46,13 +46,13 @@ describe('Add Access Controls drawer', () => { }); it('Should have a disabled Add Inbound Sources button until an inbound source field is touched', () => { + const db = { + id: 123, + engine: 'postgresql', + allow_list: IPv4List, + } as DatabaseInstance; const { getByText } = renderWithTheme( - null} - open={true} - updateDatabase={() => null} - /> + null} open={true} /> ); const addAccessControlsButton = getByText('Update Access Controls').closest( diff --git a/packages/manager/src/features/Databases/DatabaseDetail/AddAccessControlDrawer.tsx b/packages/manager/src/features/Databases/DatabaseDetail/AddAccessControlDrawer.tsx index 4788c74c5b1..e05dd1d9a8e 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/AddAccessControlDrawer.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/AddAccessControlDrawer.tsx @@ -4,17 +4,20 @@ import { useFormik } from 'formik'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; +import { Database, DatabaseInstance } from '@linode/api-v4'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; import { enforceIPMasks } from 'src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils'; +import { useDatabaseMutation } from 'src/queries/databases/databases'; import { handleAPIErrors } from 'src/utilities/formikErrorUtils'; import { ExtendedIP, extendedIPToString, ipFieldPlaceholder, + stringToExtendedIP, validateIPs, } from 'src/utilities/ipUtils'; @@ -28,10 +31,9 @@ const useStyles = makeStyles()((theme: Theme) => ({ })); interface Props { - allowList: ExtendedIP[]; + database: Database | DatabaseInstance; onClose: () => void; open: boolean; - updateDatabase: any; } interface Values { @@ -41,7 +43,7 @@ interface Values { type CombinedProps = Props; const AddAccessControlDrawer = (props: CombinedProps) => { - const { allowList, onClose, open, updateDatabase } = props; + const { database, onClose, open } = props; const { classes } = useStyles(); @@ -58,6 +60,11 @@ const AddAccessControlDrawer = (props: CombinedProps) => { setValues({ _allowList: _ipsWithMasks }); }; + const { mutateAsync: updateDatabase } = useDatabaseMutation( + database.engine, + database.id + ); + const handleUpdateAccessControlsClick = ( { _allowList }: Values, { @@ -132,7 +139,7 @@ const AddAccessControlDrawer = (props: CombinedProps) => { } = useFormik({ enableReinitialize: true, initialValues: { - _allowList: allowList, + _allowList: database?.allow_list?.map(stringToExtendedIP), }, onSubmit: handleUpdateAccessControlsClick, validate: (values: Values) => onValidate(values), @@ -182,7 +189,7 @@ const AddAccessControlDrawer = (props: CombinedProps) => { className={classes.ipSelect} forDatabaseAccessControls inputProps={{ autoFocus: true }} - ips={values._allowList} + ips={values._allowList!} onBlur={handleIPBlur} onChange={handleIPChange} placeholder={ipFieldPlaceholder} diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseActionMenu.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseActionMenu.tsx index cd39dfa610d..c5790e70432 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseActionMenu.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseActionMenu.tsx @@ -1,77 +1,68 @@ -import { Theme, useTheme } from '@mui/material/styles'; -import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; -import { makeStyles } from 'tss-react/mui'; +import { useHistory } from 'react-router-dom'; -import { Action, ActionMenu } from 'src/components/ActionMenu/ActionMenu'; -import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; +import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; -const useStyles = makeStyles()(() => ({ - root: { - alignItems: 'center', - display: 'flex', - justifyContent: 'flex-end', - padding: '0px !important', - }, -})); +import type { Engine } from '@linode/api-v4'; +import type { Action } from 'src/components/ActionMenu/ActionMenu'; -export interface ActionHandlers { - [index: string]: any; - triggerDeleteDatabase: (databaseID: number, databaseLabel: string) => void; +interface Props { + databaseEngine: Engine; + databaseId: number; + databaseLabel: string; + handlers: ActionHandlers; } -interface Props extends ActionHandlers { - databaseID: number; - databaseLabel: string; - inlineLabel?: string; +export interface ActionHandlers { + handleDelete: () => void; + handleManageAccessControls: () => void; + handleResetPassword: () => void; } -type CombinedProps = Props; +export const DatabaseActionMenu = (props: Props) => { + const { databaseEngine, databaseId, databaseLabel, handlers } = props; -const DatabaseActionMenu = (props: CombinedProps) => { - const { classes } = useStyles(); - const theme = useTheme(); - const matchesSmDown = useMediaQuery(theme.breakpoints.down('md')); + const databaseStatus = 'running'; + const isDatabaseNotRunning = databaseStatus !== 'running'; - const { databaseID, databaseLabel, triggerDeleteDatabase } = props; + const history = useHistory(); const actions: Action[] = [ + // TODO: add suspend action menu item once it's ready + // { + // onClick: () => {}, + // title: databaseStatus === 'running' ? 'Suspend' : 'Power On', + // }, + { + disabled: isDatabaseNotRunning, + onClick: handlers.handleManageAccessControls, + title: 'Manage Access Controls', + }, { + disabled: isDatabaseNotRunning, + onClick: handlers.handleResetPassword, + title: 'Reset Root Password', + }, + { + disabled: isDatabaseNotRunning, onClick: () => { - alert('Resize not yet implemented'); + history.push({ + pathname: `/databases/${databaseEngine}/${databaseId}/resize`, + }); }, title: 'Resize', }, { - onClick: () => { - if (triggerDeleteDatabase !== undefined) { - triggerDeleteDatabase(databaseID, databaseLabel); - } - }, + disabled: isDatabaseNotRunning, + onClick: handlers.handleDelete, title: 'Delete', }, ]; return ( -
- {!matchesSmDown && - actions.map((thisAction) => { - return ( - - ); - })} - {matchesSmDown && ( - - )} -
+ ); }; - -export default React.memo(DatabaseActionMenu); diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseEmptyState.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseEmptyState.tsx index 5345366f79c..f7bf2997dad 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseEmptyState.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseEmptyState.tsx @@ -4,26 +4,25 @@ import { useHistory } from 'react-router-dom'; import DatabaseIcon from 'src/assets/icons/entityIcons/database.svg'; import { ResourcesSection } from 'src/components/EmptyLandingPageResources/ResourcesSection'; import { getRestrictedResourceText } from 'src/features/Account/utils'; -import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; -import { sendEvent } from 'src/utilities/analytics/utils'; - import { gettingStartedGuides, headers, linkAnalyticsEvent, youtubeLinkData, -} from './DatabaseLandingEmptyStateData'; +} from 'src/features/Databases/DatabaseLanding/DatabaseLandingEmptyStateData'; import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; +import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; +import { sendEvent } from 'src/utilities/analytics/utils'; export const DatabaseEmptyState = () => { const { push } = useHistory(); - const { isDatabasesV2Enabled } = useIsDatabasesEnabled(); + const { isDatabasesV2Enabled, isV2GAUser } = useIsDatabasesEnabled(); const isRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_databases', }); - if (!isDatabasesV2Enabled) { + if (!isDatabasesV2Enabled || !isV2GAUser) { headers.logo = ''; } diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.test.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.test.tsx index 0d4c38ed858..df0188d426e 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.test.tsx @@ -32,6 +32,14 @@ vi.mock('src/queries/profile/profile', async () => { beforeAll(() => mockMatchMedia()); const loadingTestId = 'circle-progress'; +const accountEndpoint = '*/v4/account'; +const databaseInstancesEndpoint = '*/databases/instances'; + +const managedDBBetaCapability = 'Managed Databases Beta'; +const managedDBCapability = 'Managed Databases'; + +const newDBTabTitle = 'New Database Clusters'; +const legacyDBTabTitle = 'Legacy Database Clusters'; describe('Database Table Row', () => { it('should render a database row', () => { @@ -63,8 +71,16 @@ describe('Database Table Row', () => { describe('Database Table', () => { it('should render database landing table with items', async () => { + const mockAccount = accountFactory.build({ + capabilities: [managedDBBetaCapability], + }); + server.use( + http.get('*/account', () => { + return HttpResponse.json(mockAccount); + }) + ); server.use( - http.get('*/databases/instances', () => { + http.get(databaseInstancesEndpoint, () => { const databases = databaseInstanceFactory.buildList(1, { status: 'active', }); @@ -95,7 +111,7 @@ describe('Database Table', () => { it('should render database landing with empty state', async () => { const mockAccount = accountFactory.build({ - capabilities: ['Managed Databases Beta'], + capabilities: [managedDBBetaCapability], }); server.use( http.get('*/account', () => { @@ -103,7 +119,7 @@ describe('Database Table', () => { }) ); server.use( - http.get('*/databases/instances', () => { + http.get(databaseInstancesEndpoint, () => { return HttpResponse.json(makeResourcePage([])); }) ); @@ -122,7 +138,16 @@ describe('Database Table', () => { it('should render tabs with legacy and new databases ', async () => { server.use( - http.get('*/databases/instances', () => { + http.get(accountEndpoint, () => { + return HttpResponse.json( + accountFactory.build({ + capabilities: [managedDBCapability, managedDBBetaCapability], + }) + ); + }) + ); + server.use( + http.get(databaseInstancesEndpoint, () => { const databases = databaseInstanceFactory.buildList(5, { status: 'active', }); @@ -140,8 +165,8 @@ describe('Database Table', () => { await waitForElementToBeRemoved(getByTestId(loadingTestId)); - const newDatabasesTab = screen.getByText('New Database Clusters'); - const legacyDatabasesTab = screen.getByText('Legacy Database Clusters'); + const newDatabasesTab = screen.getByText(newDBTabTitle); + const legacyDatabasesTab = screen.getByText(legacyDBTabTitle); expect(newDatabasesTab).toBeInTheDocument(); expect(legacyDatabasesTab).toBeInTheDocument(); @@ -149,7 +174,16 @@ describe('Database Table', () => { it('should render logo in new databases tab ', async () => { server.use( - http.get('*/databases/instances', () => { + http.get(accountEndpoint, () => { + return HttpResponse.json( + accountFactory.build({ + capabilities: [managedDBCapability, managedDBBetaCapability], + }) + ); + }) + ); + server.use( + http.get(databaseInstancesEndpoint, () => { const databases = databaseInstanceFactory.buildList(5, { status: 'active', }); @@ -161,12 +195,11 @@ describe('Database Table', () => { flags: { dbaasV2: { beta: true, enabled: true } }, }); - // Loading state should render expect(getByTestId(loadingTestId)).toBeInTheDocument(); await waitForElementToBeRemoved(getByTestId(loadingTestId)); - const newDatabaseTab = screen.getByText('New Database Clusters'); + const newDatabaseTab = screen.getByText(newDBTabTitle); fireEvent.click(newDatabaseTab); expect(screen.getByText('Powered by')).toBeInTheDocument(); @@ -174,7 +207,7 @@ describe('Database Table', () => { it('should render a single legacy database table without logo ', async () => { server.use( - http.get('*/databases/instances', () => { + http.get(databaseInstancesEndpoint, () => { const databases = databaseInstanceFactory.buildList(5, { status: 'active', }); @@ -203,22 +236,23 @@ describe('Database Table', () => { false ); - expect(screen.queryByText('Legacy Database Clusters')).toBeNull(); - expect(screen.queryByText('New Database Clusters')).toBeNull(); + expect(screen.queryByText(legacyDBTabTitle)).toBeNull(); + expect(screen.queryByText(newDBTabTitle)).toBeNull(); expect(screen.queryByText('Powered by')).toBeNull(); }); it('should render a single new database table ', async () => { - const account = accountFactory.build({ - capabilities: ['Managed Databases Beta'], - }); server.use( - http.get('*/v4/account', () => { - return HttpResponse.json(account); + http.get(accountEndpoint, () => { + return HttpResponse.json( + accountFactory.build({ + capabilities: [managedDBBetaCapability], + }) + ); }) ); server.use( - http.get('*/databases/instances', () => { + http.get(databaseInstancesEndpoint, () => { const databases = databaseInstanceFactory.buildList(5, { platform: 'rdbms-default', status: 'active', @@ -238,14 +272,11 @@ describe('Database Table', () => { const tables = screen.getAllByRole('table'); expect(tables).toHaveLength(1); - const table = tables[0]; + expect(screen.getByText('Cluster Label')).toBeInTheDocument(); - const headers = within(table).getAllByRole('columnheader'); - expect(headers.some((header) => header.textContent === 'Plan')).toBe(true); - - expect(screen.queryByText('Legacy Database Clusters')).toBeNull(); - expect(screen.queryByText('New Database Clusters')).toBeNull(); - expect(screen.queryByText('Powered by')).toBeTruthy(); + expect(screen.queryByText(legacyDBTabTitle)).toBeInTheDocument(); + expect(screen.queryByText(newDBTabTitle)).toBeInTheDocument(); + expect(screen.queryByText('Powered by')).toBeNull(); }); }); @@ -279,6 +310,71 @@ describe('Database Landing', () => { expect(createClusterButton).toBeInTheDocument(); expect(createClusterButton).toHaveTextContent('Create Database Cluster'); - expect(createClusterButton).not.toBeDisabled(); + expect(createClusterButton).toBeEnabled(); + }); + + it('should render a single new database table with action menu ', async () => { + const databases = databaseInstanceFactory.buildList(5, { + platform: 'rdbms-default', + status: 'active', + }); + server.use( + http.get(databaseInstancesEndpoint, () => { + return HttpResponse.json(makeResourcePage(databases)); + }) + ); + + const { getByLabelText, getByTestId } = renderWithTheme( + , + { + flags: { dbaasV2: { beta: false, enabled: true } }, + } + ); + + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + const tables = screen.getAllByRole('table'); + expect(tables).toHaveLength(1); + + const actionMenu = getByLabelText( + `Action menu for Database ${databases[0].label}` + ); + expect(actionMenu).toBeInTheDocument(); + }); + + it('should open an action menu ', async () => { + const databases = databaseInstanceFactory.buildList(5, { + platform: 'rdbms-default', + status: 'active', + }); + server.use( + http.get(databaseInstancesEndpoint, () => { + return HttpResponse.json(makeResourcePage(databases)); + }) + ); + + const { getByLabelText, getByTestId, getByText } = renderWithTheme( + , + { + flags: { dbaasV2: { beta: false, enabled: true } }, + } + ); + + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + const actionMenu = getByLabelText( + `Action menu for Database ${databases[0].label}` + ); + + await fireEvent.click(actionMenu); + + getByText('Manage Access Controls'); + getByText('Reset Root Password'); + getByText('Resize'); + getByText('Delete'); }); }); diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.tsx index 1dff602d77d..08aa9c92a91 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.tsx @@ -12,6 +12,7 @@ import { TabList } from 'src/components/Tabs/TabList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; import { getRestrictedResourceText } from 'src/features/Account/utils'; +import { DatabaseEmptyState } from 'src/features/Databases/DatabaseLanding/DatabaseEmptyState'; import DatabaseLandingTable from 'src/features/Databases/DatabaseLanding/DatabaseLandingTable'; import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; import { DatabaseClusterInfoBanner } from 'src/features/GlobalNotifications/DatabaseClusterInfoBanner'; @@ -24,8 +25,6 @@ import { } from 'src/queries/databases/databases'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import { DatabaseEmptyState } from './DatabaseEmptyState'; - const preferenceKey = 'databases'; const DatabaseLanding = () => { @@ -37,16 +36,19 @@ const DatabaseLanding = () => { }); const { - isDatabasesV1Enabled, isDatabasesV2Enabled, isV2ExistingBetaUser, isV2GAUser, isV2NewBetaUser, } = useIsDatabasesEnabled(); + const { isLoading: isTypeLoading } = useDatabaseTypesQuery({ platform: isDatabasesV2Enabled ? 'rdbms-default' : 'rdbms-legacy', }); + const isDefaultEnabled = + isV2ExistingBetaUser || isV2NewBetaUser || isV2GAUser; + const { handleOrderChange: newDatabaseHandleOrderChange, order: newDatabaseOrder, @@ -62,12 +64,9 @@ const DatabaseLanding = () => { const newDatabasesFilter: Record = { ['+order']: newDatabaseOrder, ['+order_by']: newDatabaseOrderBy, + ['platform']: 'rdbms-default', }; - if (isV2ExistingBetaUser || isV2NewBetaUser || isV2GAUser) { - newDatabasesFilter['platform'] = 'rdbms-default'; - } - const { data: newDatabases, error: newDatabasesError, @@ -78,7 +77,7 @@ const DatabaseLanding = () => { page_size: newDatabasesPagination.pageSize, }, newDatabasesFilter, - isV2ExistingBetaUser || isV2NewBetaUser || isV2GAUser + isDefaultEnabled ); const { @@ -98,7 +97,7 @@ const DatabaseLanding = () => { ['+order_by']: legacyDatabaseOrderBy, }; - if (isDatabasesV2Enabled && isV2ExistingBetaUser) { + if (isV2ExistingBetaUser || isV2GAUser) { legacyDatabasesFilter['platform'] = 'rdbms-legacy'; } @@ -112,7 +111,7 @@ const DatabaseLanding = () => { page_size: legacyDatabasesPagination.pageSize, }, legacyDatabasesFilter, - isV2ExistingBetaUser || isDatabasesV1Enabled + !isV2NewBetaUser ); const error = newDatabasesError || legacyDatabasesError; @@ -130,16 +129,42 @@ const DatabaseLanding = () => { return ; } - const showTabs = isV2ExistingBetaUser && legacyDatabases?.data.length !== 0; - - const showEmpty = - (newDatabases?.data.length === 0 || newDatabases === undefined) && - (legacyDatabases?.data.length === 0 || legacyDatabases === undefined); - + const showEmpty = !newDatabases?.data.length && !legacyDatabases?.data.length; if (showEmpty) { return ; } + const isV2Enabled = isDatabasesV2Enabled || isV2GAUser; + const showTabs = isV2Enabled && !!legacyDatabases?.data.length; + const isNewDatabase = isV2Enabled && !!newDatabases?.data.length; + + const legacyTable = () => { + return ( + + ); + }; + + const defaultTable = () => { + return ( + + ); + }; + + const singleTable = () => { + return isNewDatabase ? defaultTable() : legacyTable(); + }; + return ( { New Database Clusters - - - - - - + {legacyTable()} + {defaultTable()} ) : ( - + singleTable() )} diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingTable.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingTable.tsx index 3837b6e835a..f198043094f 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingTable.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingTable.tsx @@ -4,12 +4,17 @@ import { Hidden } from 'src/components/Hidden'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { Table } from 'src/components/Table'; import { TableBody } from 'src/components/TableBody'; +import { TableCell } from 'src/components/TableCell'; import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow/TableRow'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableSortCell } from 'src/components/TableSortCell'; +import AddAccessControlDrawer from 'src/features/Databases/DatabaseDetail/AddAccessControlDrawer'; +import DatabaseSettingsDeleteClusterDialog from 'src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsDeleteClusterDialog'; +import DatabaseSettingsResetPasswordDialog from 'src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettingsResetPasswordDialog'; import DatabaseLogo from 'src/features/Databases/DatabaseLanding/DatabaseLogo'; import DatabaseRow from 'src/features/Databases/DatabaseLanding/DatabaseRow'; +import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; import { usePagination } from 'src/hooks/usePagination'; import { useInProgressEvents } from 'src/queries/events/events'; @@ -33,10 +38,45 @@ const DatabaseLandingTable = ({ orderBy, }: Props) => { const { data: events } = useInProgressEvents(); + const { isV2GAUser } = useIsDatabasesEnabled(); const dbPlatformType = isNewDatabase ? 'new' : 'legacy'; const pagination = usePagination(1, preferenceKey, dbPlatformType); + const [ + selectedDatabase, + setSelectedDatabase, + ] = React.useState({} as DatabaseInstance); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false); + const [ + isResetPasswordsDialogOpen, + setIsResetPasswordsDialogOpen, + ] = React.useState(false); + const [ + isManageAccessControlsDialogOpen, + setIsManageAccessControlsDialogOpen, + ] = React.useState(false); + + const handleManageAccessControls = (database: DatabaseInstance) => { + setSelectedDatabase(database); + setIsManageAccessControlsDialogOpen(true); + }; + + const onCloseAccesControls = () => { + setIsManageAccessControlsDialogOpen(false); + setSelectedDatabase({} as DatabaseInstance); + }; + + const handleDelete = (database: DatabaseInstance) => { + setSelectedDatabase(database); + setIsDeleteDialogOpen(true); + }; + + const handleResetPassword = (database: DatabaseInstance) => { + setSelectedDatabase(database); + setIsResetPasswordsDialogOpen(true); + }; + return ( <> @@ -106,11 +146,18 @@ const DatabaseLandingTable = ({ Created + {isV2GAUser && isNewDatabase && } {data?.map((database: DatabaseInstance) => ( handleDelete(database), + handleManageAccessControls: () => + handleManageAccessControls(database), + handleResetPassword: () => handleResetPassword(database), + }} database={database} events={events} isNewDatabase={isNewDatabase} @@ -137,7 +184,33 @@ const DatabaseLandingTable = ({ page={pagination.page} pageSize={pagination.pageSize} /> - {isNewDatabase && } + {isNewDatabase && ( + <> + + {selectedDatabase && ( + <> + setIsDeleteDialogOpen(false)} + open={isDeleteDialogOpen} + /> + setIsResetPasswordsDialogOpen(false)} + open={isResetPasswordsDialogOpen} + /> + + )} + + )} + ); }; diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx index 71c7dd23b1e..d8ce09f5d0d 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx @@ -6,6 +6,7 @@ import Logo from 'src/assets/icons/db-logo.svg'; import { BetaChip } from 'src/components/BetaChip/BetaChip'; import { Box } from 'src/components/Box'; import { Typography } from 'src/components/Typography'; +import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; import type { SxProps } from '@mui/material/styles'; @@ -15,6 +16,8 @@ interface Props { export const DatabaseLogo = ({ sx }: Props) => { const theme = useTheme(); + + const { isV2GAUser } = useIsDatabasesEnabled(); return ( { sx={sx ? sx : { margin: '20px' }} > - + {!isV2GAUser && ( + + )} diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx index f51a33e561e..d9e66e5f7c7 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx @@ -6,6 +6,8 @@ import { Hidden } from 'src/components/Hidden'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { DatabaseStatusDisplay } from 'src/features/Databases/DatabaseDetail/DatabaseStatusDisplay'; +import { DatabaseActionMenu } from 'src/features/Databases/DatabaseLanding/DatabaseActionMenu'; +import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; import { useDatabaseTypesQuery } from 'src/queries/databases/databases'; import { useProfile } from 'src/queries/profile/profile'; import { useRegionsQuery } from 'src/queries/regions/regions'; @@ -15,11 +17,11 @@ import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; import type { Event } from '@linode/api-v4'; import type { - Database, DatabaseInstance, DatabaseType, Engine, } from '@linode/api-v4/lib/databases/types'; +import type { ActionHandlers } from 'src/features/Databases/DatabaseLanding/DatabaseActionMenu'; export const databaseEngineMap: Record = { mongodb: 'MongoDB', @@ -29,12 +31,22 @@ export const databaseEngineMap: Record = { }; interface Props { - database: Database | DatabaseInstance; + database: DatabaseInstance; events?: Event[]; + /** + * Not used for V1, will be required once migration is complete + * @since DBaaS V2 GA + */ + handlers?: ActionHandlers; isNewDatabase?: boolean; } -export const DatabaseRow = ({ database, events, isNewDatabase }: Props) => { +export const DatabaseRow = ({ + database, + events, + handlers, + isNewDatabase, +}: Props) => { const { cluster_size, created, @@ -54,6 +66,7 @@ export const DatabaseRow = ({ database, events, isNewDatabase }: Props) => { const plan = types?.find((t: DatabaseType) => t.id === type); const formattedPlan = plan && formatStorageUnits(plan.label); const actualRegion = regions?.find((r) => r.id === region); + const { isV2GAUser } = useIsDatabasesEnabled(); const configuration = cluster_size === 1 ? ( @@ -69,7 +82,6 @@ export const DatabaseRow = ({ database, events, isNewDatabase }: Props) => { /> ); - return ( @@ -95,6 +107,16 @@ export const DatabaseRow = ({ database, events, isNewDatabase }: Props) => { })} + {isV2GAUser && isNewDatabase && ( + + + + )} ); }; diff --git a/packages/manager/src/features/GlobalNotifications/DatabaseClusterInfoBanner.tsx b/packages/manager/src/features/GlobalNotifications/DatabaseClusterInfoBanner.tsx index 9b3c4780a1b..dd09facc8b8 100644 --- a/packages/manager/src/features/GlobalNotifications/DatabaseClusterInfoBanner.tsx +++ b/packages/manager/src/features/GlobalNotifications/DatabaseClusterInfoBanner.tsx @@ -28,12 +28,12 @@ export const DatabaseClusterInfoBanner = () => {
  • - As a Beta customer you can only create Aiven Database clusters. + As a Beta customer you can only create New Database clusters
  • - You won’t be charged for Aiven Database clusters created during + You won’t be charged for New Database clusters created during duration of the Beta phase. If you decide to keep the new clusters later on, you’ll be charged according to the new payment. You can always delete unwanted clusters.