diff --git a/packages/manager/.changeset/pr-11059-added-1728379220045.md b/packages/manager/.changeset/pr-11059-added-1728379220045.md new file mode 100644 index 00000000000..6fcbbb2149b --- /dev/null +++ b/packages/manager/.changeset/pr-11059-added-1728379220045.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Databases to search result queries ([#11059](https://github.com/linode/manager/pull/11059)) diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.tsx index 1c39a908a3e..721633578d9 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.tsx @@ -1,10 +1,3 @@ -import { Region } from '@linode/api-v4'; -import { - Database, - DatabaseInstance, - DatabaseType, -} from '@linode/api-v4/lib/databases/types'; -import { Theme } from '@mui/material/styles'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; @@ -20,6 +13,14 @@ import { convertMegabytesTo } from 'src/utilities/unitConversions'; import { databaseEngineMap } from '../../DatabaseLanding/DatabaseRow'; import { DatabaseStatusDisplay } from '../DatabaseStatusDisplay'; +import type { Region } from '@linode/api-v4'; +import type { + Database, + DatabaseInstance, + DatabaseType, +} from '@linode/api-v4/lib/databases/types'; +import type { Theme } from '@mui/material/styles'; + const useStyles = makeStyles()((theme: Theme) => ({ configs: { fontSize: '0.875rem', diff --git a/packages/manager/src/features/Databases/utilities.ts b/packages/manager/src/features/Databases/utilities.ts index d1b6cea6b49..7272b939185 100644 --- a/packages/manager/src/features/Databases/utilities.ts +++ b/packages/manager/src/features/Databases/utilities.ts @@ -6,6 +6,10 @@ import { useAccount } from 'src/queries/account/account'; import { useDatabaseEnginesQuery } from 'src/queries/databases/databases'; import { isFeatureEnabledV2 } from 'src/utilities/accountCapabilities'; +import { databaseEngineMap } from './DatabaseLanding/DatabaseRow'; + +import type { DatabaseInstance } from '@linode/api-v4'; + /** * A hook to determine if Databases should be visible to the user. * @@ -180,3 +184,7 @@ export const toDatabaseFork = ( } return fork; }; + +export const getDatabasesDescription = (database: DatabaseInstance) => { + return `${databaseEngineMap[database.engine]} v${database.version}`; +}; diff --git a/packages/manager/src/features/Search/SearchLanding.tsx b/packages/manager/src/features/Search/SearchLanding.tsx index a151e31fa19..db4384d0e6e 100644 --- a/packages/manager/src/features/Search/SearchLanding.tsx +++ b/packages/manager/src/features/Search/SearchLanding.tsx @@ -9,6 +9,7 @@ import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; import { useAPISearch } from 'src/features/Search/useAPISearch'; import { useIsLargeAccount } from 'src/hooks/useIsLargeAccount'; +import { useAllDatabasesQuery } from 'src/queries/databases/databases'; import { useAllDomainsQuery } from 'src/queries/domains'; import { useAllFirewallsQuery } from 'src/queries/firewalls'; import { useAllImagesQuery } from 'src/queries/images'; @@ -42,9 +43,11 @@ import withStoreSearch from './withStoreSearch'; import type { SearchProps } from './withStoreSearch'; import type { RouteComponentProps } from 'react-router-dom'; +import { useIsDatabasesEnabled } from '../Databases/utilities'; const displayMap = { buckets: 'Buckets', + databases: 'Databases', domains: 'Domains', firewalls: 'Firewalls', images: 'Images', @@ -63,12 +66,16 @@ export const SearchLanding = (props: SearchLandingProps) => { const { data: regions } = useRegionsQuery(); const isLargeAccount = useIsLargeAccount(); + const { isDatabasesEnabled } = useIsDatabasesEnabled(); // We only want to fetch all entities if we know they // are not a large account. We do this rather than `!isLargeAccount` // because we don't want to fetch all entities if isLargeAccount is loading (undefined). const shouldFetchAllEntities = isLargeAccount === false; + const shouldMakeDBRequests = + shouldFetchAllEntities && Boolean(isDatabasesEnabled); + /* @TODO OBJ Multicluster:'region' will become required, and the 'cluster' field will be deprecated once the feature is fully rolled out in production. @@ -81,6 +88,16 @@ export const SearchLanding = (props: SearchLandingProps) => { isLoading: areBucketsLoading, } = useObjectStorageBuckets(shouldFetchAllEntities); + /* + @TODO DBaaS: Change the passed argument to 'shouldFetchAllEntities' and + remove 'isDatabasesEnabled' once DBaaS V2 is fully rolled out. + */ + const { + data: databases, + error: databasesError, + isLoading: areDatabasesLoading, + } = useAllDatabasesQuery(shouldMakeDBRequests); + const { data: domains, error: domainsError, @@ -186,7 +203,8 @@ export const SearchLanding = (props: SearchLandingProps) => { regions ?? [], searchableLinodes ?? [], nodebalancers ?? [], - firewalls ?? [] + firewalls ?? [], + databases ?? [] ); } }, [ @@ -204,6 +222,7 @@ export const SearchLanding = (props: SearchLandingProps) => { nodebalancers, linodes, firewalls, + databases, ]); const getErrorMessage = () => { @@ -216,6 +235,7 @@ export const SearchLanding = (props: SearchLandingProps) => { [nodebalancersError, 'NodeBalancers'], [kubernetesClustersError, 'Kubernetes'], [firewallsError, 'Firewalls'], + [databasesError, 'Databases'], [ objectStorageBuckets && objectStorageBuckets.errors.length > 0, `Object Storage in ${objectStorageBuckets?.errors @@ -250,7 +270,8 @@ export const SearchLanding = (props: SearchLandingProps) => { areKubernetesClustersLoading || areImagesLoading || areNodeBalancersLoading || - areFirewallsLoading; + areFirewallsLoading || + areDatabasesLoading; const errorMessage = getErrorMessage(); diff --git a/packages/manager/src/features/Search/refinedSearch.ts b/packages/manager/src/features/Search/refinedSearch.ts index 6fd093981f1..2624d2fce4c 100644 --- a/packages/manager/src/features/Search/refinedSearch.ts +++ b/packages/manager/src/features/Search/refinedSearch.ts @@ -2,7 +2,7 @@ import logicQueryParser from 'logic-query-parser'; import { all, any, equals, isEmpty } from 'ramda'; import searchString from 'search-string'; -import { SearchField, SearchableItem } from './search.interfaces'; +import type { SearchField, SearchableItem } from './search.interfaces'; export const COMPRESSED_IPV6_REGEX = /^([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,7})?::([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,7})?$/; const DEFAULT_SEARCH_FIELDS = ['label', 'tags', 'ips']; diff --git a/packages/manager/src/features/Search/search.interfaces.ts b/packages/manager/src/features/Search/search.interfaces.ts index e9e06ec54b6..e8c45d5c334 100644 --- a/packages/manager/src/features/Search/search.interfaces.ts +++ b/packages/manager/src/features/Search/search.interfaces.ts @@ -12,6 +12,7 @@ export interface SearchableItem { export type SearchableEntityType = | 'bucket' + | 'database' | 'domain' | 'firewall' | 'image' @@ -25,6 +26,7 @@ export type SearchField = 'ips' | 'label' | 'tags' | 'type'; export interface SearchResultsByEntity { buckets: SearchableItem[]; + databases: SearchableItem[]; domains: SearchableItem[]; firewalls: SearchableItem[]; images: SearchableItem[]; diff --git a/packages/manager/src/features/Search/utils.test.ts b/packages/manager/src/features/Search/utils.test.ts index aec6c0007b7..4162c8a96ad 100644 --- a/packages/manager/src/features/Search/utils.test.ts +++ b/packages/manager/src/features/Search/utils.test.ts @@ -17,6 +17,7 @@ describe('separate results by entity', () => { expect(results).toHaveProperty('kubernetesClusters'); expect(results).toHaveProperty('buckets'); expect(results).toHaveProperty('firewalls'); + expect(results).toHaveProperty('databases'); }); it('the value of each entity type is an array', () => { @@ -28,12 +29,14 @@ describe('separate results by entity', () => { expect(results.kubernetesClusters).toBeInstanceOf(Array); expect(results.buckets).toBeInstanceOf(Array); expect(results.firewalls).toBeInstanceOf(Array); + expect(results.databases).toBeInstanceOf(Array); }); it('returns empty results if there is no data', () => { const newResults = separateResultsByEntity([]); expect(newResults).toEqual({ buckets: [], + databases: [], domains: [], firewalls: [], images: [], diff --git a/packages/manager/src/features/Search/utils.ts b/packages/manager/src/features/Search/utils.ts index 024d955dab0..0361e378d31 100644 --- a/packages/manager/src/features/Search/utils.ts +++ b/packages/manager/src/features/Search/utils.ts @@ -5,6 +5,7 @@ import type { export const emptyResults: SearchResultsByEntity = { buckets: [], + databases: [], domains: [], firewalls: [], images: [], @@ -19,6 +20,7 @@ export const separateResultsByEntity = ( ): SearchResultsByEntity => { const separatedResults: SearchResultsByEntity = { buckets: [], + databases: [], domains: [], firewalls: [], images: [], diff --git a/packages/manager/src/features/Search/withStoreSearch.tsx b/packages/manager/src/features/Search/withStoreSearch.tsx index a8f4dd664a8..259b0c46b69 100644 --- a/packages/manager/src/features/Search/withStoreSearch.tsx +++ b/packages/manager/src/features/Search/withStoreSearch.tsx @@ -3,6 +3,7 @@ import { compose, withStateHandlers } from 'recompose'; import { bucketToSearchableItem, + databaseToSearchableItem, domainToSearchableItem, firewallToSearchableItem, imageToSearchableItem, @@ -20,6 +21,7 @@ import type { SearchableItem, } from './search.interfaces'; import type { + DatabaseInstance, Firewall, Image, KubernetesCluster, @@ -41,7 +43,8 @@ interface HandlerProps { regions: Region[], searchableLinodes: SearchableItem[], nodebalancers: NodeBalancer[], - firewalls: Firewall[] + firewalls: Firewall[], + databases: DatabaseInstance[] ) => SearchResults; } export interface SearchProps extends HandlerProps { @@ -88,7 +91,8 @@ export default () => (Component: React.ComponentType) => { regions: Region[], searchableLinodes: SearchableItem[], nodebalancers: NodeBalancer[], - firewalls: Firewall[] + firewalls: Firewall[], + databases: DatabaseInstance[] ) => { const searchableBuckets = objectStorageBuckets.map((bucket) => bucketToSearchableItem(bucket) @@ -111,6 +115,9 @@ export default () => (Component: React.ComponentType) => { const searchableFirewalls = firewalls.map((firewall) => firewallToSearchableItem(firewall) ); + const searchableDatabases = databases.map((database) => + databaseToSearchableItem(database) + ); const results = search( [ ...searchableLinodes, @@ -121,6 +128,7 @@ export default () => (Component: React.ComponentType) => { ...searchableClusters, ...searchableNodebalancers, ...searchableFirewalls, + ...searchableDatabases, ], query ); diff --git a/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx b/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx index 77df0b78608..b6a53bb8130 100644 --- a/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx +++ b/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx @@ -7,10 +7,12 @@ import { components } from 'react-select'; import { debounce } from 'throttle-debounce'; import EnhancedSelect from 'src/components/EnhancedSelect/Select'; +import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; import { getImageLabelForLinode } from 'src/features/Images/utils'; import { useAPISearch } from 'src/features/Search/useAPISearch'; import withStoreSearch from 'src/features/Search/withStoreSearch'; import { useIsLargeAccount } from 'src/hooks/useIsLargeAccount'; +import { useAllDatabasesQuery } from 'src/queries/databases/databases'; import { useAllDomainsQuery } from 'src/queries/domains'; import { useAllFirewallsQuery } from 'src/queries/firewalls'; import { useAllImagesQuery } from 'src/queries/images'; @@ -86,12 +88,16 @@ const SearchBar = (props: SearchProps) => { const [apiSearchLoading, setAPILoading] = React.useState(false); const history = useHistory(); const isLargeAccount = useIsLargeAccount(searchActive); + const { isDatabasesEnabled } = useIsDatabasesEnabled(); // Only request things if the search bar is open/active and we // know if the account is large or not const shouldMakeRequests = searchActive && isLargeAccount !== undefined && !isLargeAccount; + const shouldMakeDBRequests = + shouldMakeRequests && Boolean(isDatabasesEnabled); + const { data: regions } = useRegionsQuery(); const { data: objectStorageBuckets } = useObjectStorageBuckets( @@ -103,6 +109,13 @@ const SearchBar = (props: SearchProps) => { const { data: volumes } = useAllVolumesQuery({}, {}, shouldMakeRequests); const { data: nodebalancers } = useAllNodeBalancersQuery(shouldMakeRequests); const { data: firewalls } = useAllFirewallsQuery(shouldMakeRequests); + + /* + @TODO DBaaS: Change the passed argument to 'shouldMakeRequests' and + remove 'isDatabasesEnabled' once DBaaS V2 is fully rolled out. + */ + const { data: databases } = useAllDatabasesQuery(shouldMakeDBRequests); + const { data: _privateImages, isLoading: imagesLoading } = useAllImagesQuery( {}, { is_public: false }, // We want to display private images (i.e., not Debian, Ubuntu, etc. distros) @@ -186,7 +199,8 @@ const SearchBar = (props: SearchProps) => { regions ?? [], searchableLinodes ?? [], nodebalancers ?? [], - firewalls ?? [] + firewalls ?? [], + databases ?? [] ); } }, [ @@ -202,6 +216,7 @@ const SearchBar = (props: SearchProps) => { regions, nodebalancers, firewalls, + databases, ]); const handleSearchChange = (_searchText: string): void => { diff --git a/packages/manager/src/store/selectors/getSearchEntities.ts b/packages/manager/src/store/selectors/getSearchEntities.ts index a8f2187a68c..f7cbef079f3 100644 --- a/packages/manager/src/store/selectors/getSearchEntities.ts +++ b/packages/manager/src/store/selectors/getSearchEntities.ts @@ -1,3 +1,4 @@ +import { getDatabasesDescription } from 'src/features/Databases/utilities'; import { getFirewallDescription } from 'src/features/Firewalls/shared'; import { getDescriptionForCluster } from 'src/features/Kubernetes/kubeUtils'; import { displayType } from 'src/features/Linodes/presentation'; @@ -5,6 +6,7 @@ import { getLinodeDescription } from 'src/utilities/getLinodeDescription'; import { readableBytes } from 'src/utilities/unitConversions'; import type { + DatabaseInstance, Domain, Firewall, Image, @@ -179,3 +181,19 @@ export const firewallToSearchableItem = ( label: firewall.label, value: firewall.id, }); + +export const databaseToSearchableItem = ( + database: DatabaseInstance +): SearchableItem => ({ + data: { + created: database.created, + description: getDatabasesDescription(database), + icon: 'database', + path: `/databases/${database.engine}/${database.id}`, + region: database.region, + status: database.status, + }, + entityType: 'database', + label: database.label, + value: `${database.engine}/${database.id}`, +});