From 0ed20a31405417069fee423e070f4a09927041c2 Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Tue, 24 Sep 2024 18:44:08 +0200 Subject: [PATCH] feat: [UIE-8098] - DBaaS GA Landing Page --- .../pr-11039-added-1727885059968.md | 5 + packages/api-v4/src/databases/types.ts | 1 + ...r-11039-upcoming-features-1727885138241.md | 5 + packages/manager/src/factories/databases.ts | 1 + .../DatabaseDetail/AccessControls.tsx | 29 +--- .../AddAccessControlDrawer.test.tsx | 26 ++-- .../DatabaseDetail/AddAccessControlDrawer.tsx | 17 ++- .../DatabaseLanding/DatabaseActionMenu.tsx | 95 ++++++------ .../DatabaseLanding/DatabaseEmptyState.tsx | 11 +- .../DatabaseLanding/DatabaseLanding.test.tsx | 142 ++++++++++++++---- .../DatabaseLanding/DatabaseLanding.tsx | 94 ++++++------ .../DatabaseLanding/DatabaseLandingTable.tsx | 75 ++++++++- .../DatabaseLanding/DatabaseLogo.tsx | 29 ++-- .../Databases/DatabaseLanding/DatabaseRow.tsx | 30 +++- 14 files changed, 366 insertions(+), 194 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-11039-added-1727885059968.md create mode 100644 packages/manager/.changeset/pr-11039-upcoming-features-1727885138241.md 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..91ac08db93a 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..2af2b2e740e 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', () => { @@ -64,7 +72,7 @@ describe('Database Table Row', () => { describe('Database Table', () => { it('should render database landing table with items', async () => { server.use( - http.get('*/databases/instances', () => { + http.get(databaseInstancesEndpoint, () => { const databases = databaseInstanceFactory.buildList(1, { status: 'active', }); @@ -95,7 +103,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 +111,7 @@ describe('Database Table', () => { }) ); server.use( - http.get('*/databases/instances', () => { + http.get(databaseInstancesEndpoint, () => { return HttpResponse.json(makeResourcePage([])); }) ); @@ -122,7 +130,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 +157,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 +166,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 +187,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 +199,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 +228,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'], - }); + it.only('should render a single new database table ', async () => { 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 +264,11 @@ describe('Database Table', () => { const tables = screen.getAllByRole('table'); expect(tables).toHaveLength(1); - const table = tables[0]; - - const headers = within(table).getAllByRole('columnheader'); - expect(headers.some((header) => header.textContent === 'Plan')).toBe(true); + expect(screen.getByText('Cluster Label')).toBeInTheDocument(); - 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 +302,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..bc6324ca293 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(); + 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(null); + }; + + 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 && } {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..d0d60cd4b2d 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 && ( + + + + )} ); };