From d1edee6c3a4ca4845975938c2bad5cb55ed990ad Mon Sep 17 00:00:00 2001 From: Purvesh Makode Date: Mon, 7 Oct 2024 12:10:57 +0530 Subject: [PATCH] feat: [M3-7011] - Add firewalls to search result queries (#11023) * Add firewalls to search result queries * Added changeset: Firewalls to search result queries * Update tests * Change search bar text and update e2e tests * Added changeset: Firewall attributes 'created_dt' to 'created' and 'updated_dt' to 'updated' * Add customized Firewall description * Move getFirewallDescription to Firewalls/shared.ts --- .../pr-11023-changed-1727944517923.md | 5 +++ packages/api-v4/src/firewalls/types.ts | 4 +- .../pr-11023-added-1727785979860.md | 5 +++ .../core/linodes/smoke-delete-linode.spec.ts | 4 +- .../smoke-linode-landing-table.spec.ts | 4 +- packages/manager/src/__data__/firewalls.ts | 12 +++--- packages/manager/src/factories/firewalls.ts | 4 +- .../manager/src/features/Firewalls/shared.ts | 13 ++++++- .../src/features/Search/SearchLanding.tsx | 16 +++++++- .../src/features/Search/search.interfaces.ts | 2 + .../manager/src/features/Search/utils.test.ts | 6 ++- packages/manager/src/features/Search/utils.ts | 7 +++- .../src/features/Search/withStoreSearch.tsx | 37 +++++++++++-------- .../features/TopMenu/SearchBar/SearchBar.tsx | 10 +++-- .../src/store/selectors/getSearchEntities.ts | 17 +++++++++ 15 files changed, 106 insertions(+), 40 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-11023-changed-1727944517923.md create mode 100644 packages/manager/.changeset/pr-11023-added-1727785979860.md diff --git a/packages/api-v4/.changeset/pr-11023-changed-1727944517923.md b/packages/api-v4/.changeset/pr-11023-changed-1727944517923.md new file mode 100644 index 00000000000..61d1b45dcc6 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11023-changed-1727944517923.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +Firewall attributes 'created_dt' to 'created' and 'updated_dt' to 'updated' ([#11023](https://github.com/linode/manager/pull/11023)) diff --git a/packages/api-v4/src/firewalls/types.ts b/packages/api-v4/src/firewalls/types.ts index f5fc7d0bb90..53bd059841b 100644 --- a/packages/api-v4/src/firewalls/types.ts +++ b/packages/api-v4/src/firewalls/types.ts @@ -12,8 +12,8 @@ export interface Firewall { label: string; tags: string[]; rules: FirewallRules; - created_dt: string; - updated_dt: string; + created: string; + updated: string; entities: { id: number; type: FirewallDeviceEntityType; diff --git a/packages/manager/.changeset/pr-11023-added-1727785979860.md b/packages/manager/.changeset/pr-11023-added-1727785979860.md new file mode 100644 index 00000000000..6e3d682ca09 --- /dev/null +++ b/packages/manager/.changeset/pr-11023-added-1727785979860.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Firewalls to search result queries ([#11023](https://github.com/linode/manager/pull/11023)) diff --git a/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts index 8eaec582df7..9f2dd3731d9 100644 --- a/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts @@ -14,9 +14,7 @@ const confirmDeletion = (linodeLabel: string) => { cy.findByText(linodeLabel).should('not.exist'); // Confirm the linode instance is removed - cy.findByText( - 'Search for Linodes, Volumes, NodeBalancers, Domains, Buckets, Tags...' - ) + cy.findByText('Search Products, IP Addresses, Tags...') .click() .type(`${linodeLabel}{enter}`); cy.findByText('You searched for ...').should('be.visible'); diff --git a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts index d654d58ea32..df9e3afb392 100644 --- a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts @@ -113,9 +113,7 @@ describe('linode landing checks', () => { getVisible('[aria-label="open menu"]'); getVisible('[data-qa-add-new-menu-button="true"]'); getVisible('[data-qa-search-icon="true"]'); - fbtVisible( - 'Search for Linodes, Volumes, NodeBalancers, Domains, Buckets, Tags...' - ); + fbtVisible('Search Products, IP Addresses, Tags...'); cy.findByLabelText('Help & Support') .should('be.visible') diff --git a/packages/manager/src/__data__/firewalls.ts b/packages/manager/src/__data__/firewalls.ts index aed2bbdb1eb..6f7c7c9627a 100644 --- a/packages/manager/src/__data__/firewalls.ts +++ b/packages/manager/src/__data__/firewalls.ts @@ -1,8 +1,8 @@ -import { Firewall } from '@linode/api-v4/lib/firewalls'; -import { FirewallDeviceEntityType } from '@linode/api-v4/lib/firewalls'; +import type { Firewall } from '@linode/api-v4/lib/firewalls'; +import type { FirewallDeviceEntityType } from '@linode/api-v4/lib/firewalls'; export const firewall: Firewall = { - created_dt: '2019-09-11T19:44:38.526Z', + created: '2019-09-11T19:44:38.526Z', entities: [ { id: 1, @@ -37,11 +37,11 @@ export const firewall: Firewall = { }, status: 'enabled', tags: [], - updated_dt: '2019-09-11T19:44:38.526Z', + updated: '2019-09-11T19:44:38.526Z', }; export const firewall2: Firewall = { - created_dt: '2019-12-11T19:44:38.526Z', + created: '2019-12-11T19:44:38.526Z', entities: [ { id: 1, @@ -71,7 +71,7 @@ export const firewall2: Firewall = { }, status: 'disabled', tags: [], - updated_dt: '2019-12-11T19:44:38.526Z', + updated: '2019-12-11T19:44:38.526Z', }; export const firewalls = [firewall, firewall2]; diff --git a/packages/manager/src/factories/firewalls.ts b/packages/manager/src/factories/firewalls.ts index 85591dcbc18..dfd962df981 100644 --- a/packages/manager/src/factories/firewalls.ts +++ b/packages/manager/src/factories/firewalls.ts @@ -29,7 +29,7 @@ export const firewallRulesFactory = Factory.Sync.makeFactory({ }); export const firewallFactory = Factory.Sync.makeFactory({ - created_dt: '2020-01-01 00:00:00', + created: '2020-01-01 00:00:00', entities: [ { id: 1, @@ -43,7 +43,7 @@ export const firewallFactory = Factory.Sync.makeFactory({ rules: firewallRulesFactory.build(), status: 'enabled', tags: [], - updated_dt: '2020-01-01 00:00:00', + updated: '2020-01-01 00:00:00', }); export const firewallDeviceFactory = Factory.Sync.makeFactory({ diff --git a/packages/manager/src/features/Firewalls/shared.ts b/packages/manager/src/features/Firewalls/shared.ts index 4fd4fc6c04d..3f4f8a6555f 100644 --- a/packages/manager/src/features/Firewalls/shared.ts +++ b/packages/manager/src/features/Firewalls/shared.ts @@ -1,11 +1,14 @@ +import { capitalize } from 'src/utilities/capitalize'; import { truncateAndJoinList } from 'src/utilities/stringUtils'; +import { PORT_PRESETS } from './FirewallDetail/Rules/shared'; + import type { Grants, Profile } from '@linode/api-v4'; import type { + Firewall, FirewallRuleProtocol, FirewallRuleType, } from '@linode/api-v4/lib/firewalls/types'; -import { PORT_PRESETS } from './FirewallDetail/Rules/shared'; export type FirewallPreset = 'dns' | 'http' | 'https' | 'mysql' | 'ssh'; @@ -258,3 +261,11 @@ export const checkIfUserCanModifyFirewall = ( ?.permissions === 'read_write' ); }; + +export const getFirewallDescription = (firewall: Firewall) => { + const description = [ + `Status: ${capitalize(firewall.status)}`, + `Services Assigned: ${firewall.entities.length}`, + ]; + return description.join(', '); +}; diff --git a/packages/manager/src/features/Search/SearchLanding.tsx b/packages/manager/src/features/Search/SearchLanding.tsx index a9f6205c1fc..a151e31fa19 100644 --- a/packages/manager/src/features/Search/SearchLanding.tsx +++ b/packages/manager/src/features/Search/SearchLanding.tsx @@ -10,6 +10,7 @@ import { Typography } from 'src/components/Typography'; import { useAPISearch } from 'src/features/Search/useAPISearch'; import { useIsLargeAccount } from 'src/hooks/useIsLargeAccount'; import { useAllDomainsQuery } from 'src/queries/domains'; +import { useAllFirewallsQuery } from 'src/queries/firewalls'; import { useAllImagesQuery } from 'src/queries/images'; import { useAllKubernetesClustersQuery } from 'src/queries/kubernetes'; import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; @@ -45,6 +46,7 @@ import type { RouteComponentProps } from 'react-router-dom'; const displayMap = { buckets: 'Buckets', domains: 'Domains', + firewalls: 'Firewalls', images: 'Images', kubernetesClusters: 'Kubernetes', linodes: 'Linodes', @@ -85,6 +87,12 @@ export const SearchLanding = (props: SearchLandingProps) => { isLoading: areDomainsLoading, } = useAllDomainsQuery(shouldFetchAllEntities); + const { + data: firewalls, + error: firewallsError, + isLoading: areFirewallsLoading, + } = useAllFirewallsQuery(shouldFetchAllEntities); + const { data: kubernetesClusters, error: kubernetesClustersError, @@ -177,7 +185,8 @@ export const SearchLanding = (props: SearchLandingProps) => { _privateImages ?? [], regions ?? [], searchableLinodes ?? [], - nodebalancers ?? [] + nodebalancers ?? [], + firewalls ?? [] ); } }, [ @@ -194,6 +203,7 @@ export const SearchLanding = (props: SearchLandingProps) => { regions, nodebalancers, linodes, + firewalls, ]); const getErrorMessage = () => { @@ -205,6 +215,7 @@ export const SearchLanding = (props: SearchLandingProps) => { [imagesError, 'Images'], [nodebalancersError, 'NodeBalancers'], [kubernetesClustersError, 'Kubernetes'], + [firewallsError, 'Firewalls'], [ objectStorageBuckets && objectStorageBuckets.errors.length > 0, `Object Storage in ${objectStorageBuckets?.errors @@ -238,7 +249,8 @@ export const SearchLanding = (props: SearchLandingProps) => { areVolumesLoading || areKubernetesClustersLoading || areImagesLoading || - areNodeBalancersLoading; + areNodeBalancersLoading || + areFirewallsLoading; const errorMessage = getErrorMessage(); diff --git a/packages/manager/src/features/Search/search.interfaces.ts b/packages/manager/src/features/Search/search.interfaces.ts index 5a30fc93939..e9e06ec54b6 100644 --- a/packages/manager/src/features/Search/search.interfaces.ts +++ b/packages/manager/src/features/Search/search.interfaces.ts @@ -13,6 +13,7 @@ export interface SearchableItem { export type SearchableEntityType = | 'bucket' | 'domain' + | 'firewall' | 'image' | 'kubernetesCluster' | 'linode' @@ -25,6 +26,7 @@ export type SearchField = 'ips' | 'label' | 'tags' | 'type'; export interface SearchResultsByEntity { buckets: SearchableItem[]; domains: SearchableItem[]; + firewalls: SearchableItem[]; images: SearchableItem[]; kubernetesClusters: SearchableItem[]; linodes: SearchableItem[]; diff --git a/packages/manager/src/features/Search/utils.test.ts b/packages/manager/src/features/Search/utils.test.ts index e4ebe3ba596..aec6c0007b7 100644 --- a/packages/manager/src/features/Search/utils.test.ts +++ b/packages/manager/src/features/Search/utils.test.ts @@ -1,8 +1,9 @@ import { searchableItems } from 'src/__data__/searchableItems'; -import { SearchableItem } from 'src/features/Search/search.interfaces'; import { separateResultsByEntity } from './utils'; +import type { SearchableItem } from 'src/features/Search/search.interfaces'; + const data = searchableItems as SearchableItem[]; describe('separate results by entity', () => { @@ -15,6 +16,7 @@ describe('separate results by entity', () => { expect(results).toHaveProperty('nodebalancers'); expect(results).toHaveProperty('kubernetesClusters'); expect(results).toHaveProperty('buckets'); + expect(results).toHaveProperty('firewalls'); }); it('the value of each entity type is an array', () => { @@ -25,6 +27,7 @@ describe('separate results by entity', () => { expect(results.nodebalancers).toBeInstanceOf(Array); expect(results.kubernetesClusters).toBeInstanceOf(Array); expect(results.buckets).toBeInstanceOf(Array); + expect(results.firewalls).toBeInstanceOf(Array); }); it('returns empty results if there is no data', () => { @@ -32,6 +35,7 @@ describe('separate results by entity', () => { expect(newResults).toEqual({ buckets: [], domains: [], + firewalls: [], images: [], kubernetesClusters: [], linodes: [], diff --git a/packages/manager/src/features/Search/utils.ts b/packages/manager/src/features/Search/utils.ts index 4b23ac68683..024d955dab0 100644 --- a/packages/manager/src/features/Search/utils.ts +++ b/packages/manager/src/features/Search/utils.ts @@ -1,8 +1,12 @@ -import { SearchResultsByEntity, SearchableItem } from './search.interfaces'; +import type { + SearchResultsByEntity, + SearchableItem, +} from './search.interfaces'; export const emptyResults: SearchResultsByEntity = { buckets: [], domains: [], + firewalls: [], images: [], kubernetesClusters: [], linodes: [], @@ -16,6 +20,7 @@ export const separateResultsByEntity = ( const separatedResults: SearchResultsByEntity = { buckets: [], domains: [], + firewalls: [], images: [], kubernetesClusters: [], linodes: [], diff --git a/packages/manager/src/features/Search/withStoreSearch.tsx b/packages/manager/src/features/Search/withStoreSearch.tsx index 629eac35d00..a8f4dd664a8 100644 --- a/packages/manager/src/features/Search/withStoreSearch.tsx +++ b/packages/manager/src/features/Search/withStoreSearch.tsx @@ -1,18 +1,10 @@ -import { - Image, - KubernetesCluster, - NodeBalancer, - Region, - Volume, -} from '@linode/api-v4'; -import { Domain } from '@linode/api-v4/lib/domains'; -import { ObjectStorageBucket } from '@linode/api-v4/lib/object-storage'; import * as React from 'react'; import { compose, withStateHandlers } from 'recompose'; import { bucketToSearchableItem, domainToSearchableItem, + firewallToSearchableItem, imageToSearchableItem, kubernetesClusterToSearchableItem, nodeBalToSearchableItem, @@ -20,12 +12,23 @@ import { } from 'src/store/selectors/getSearchEntities'; import { refinedSearch } from './refinedSearch'; -import { +import { emptyResults, separateResultsByEntity } from './utils'; + +import type { SearchResults, SearchResultsByEntity, SearchableItem, } from './search.interfaces'; -import { emptyResults, separateResultsByEntity } from './utils'; +import type { + Firewall, + Image, + KubernetesCluster, + NodeBalancer, + Region, + Volume, +} from '@linode/api-v4'; +import type { Domain } from '@linode/api-v4/lib/domains'; +import type { ObjectStorageBucket } from '@linode/api-v4/lib/object-storage'; interface HandlerProps { search: ( @@ -37,7 +40,8 @@ interface HandlerProps { images: Image[], regions: Region[], searchableLinodes: SearchableItem[], - nodebalancers: NodeBalancer[] + nodebalancers: NodeBalancer[], + firewalls: Firewall[] ) => SearchResults; } export interface SearchProps extends HandlerProps { @@ -83,7 +87,8 @@ export default () => (Component: React.ComponentType) => { images: Image[], regions: Region[], searchableLinodes: SearchableItem[], - nodebalancers: NodeBalancer[] + nodebalancers: NodeBalancer[], + firewalls: Firewall[] ) => { const searchableBuckets = objectStorageBuckets.map((bucket) => bucketToSearchableItem(bucket) @@ -97,14 +102,15 @@ export default () => (Component: React.ComponentType) => { const searchableImages = images.map((image) => imageToSearchableItem(image) ); - const searchableClusters = clusters.map((cluster) => kubernetesClusterToSearchableItem(cluster, regions) ); - const searchableNodebalancers = nodebalancers.map((nodebalancer) => nodeBalToSearchableItem(nodebalancer) ); + const searchableFirewalls = firewalls.map((firewall) => + firewallToSearchableItem(firewall) + ); const results = search( [ ...searchableLinodes, @@ -114,6 +120,7 @@ export default () => (Component: React.ComponentType) => { ...searchableVolumes, ...searchableClusters, ...searchableNodebalancers, + ...searchableFirewalls, ], query ); diff --git a/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx b/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx index 23e5d660889..77df0b78608 100644 --- a/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx +++ b/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx @@ -12,6 +12,7 @@ import { useAPISearch } from 'src/features/Search/useAPISearch'; import withStoreSearch from 'src/features/Search/withStoreSearch'; import { useIsLargeAccount } from 'src/hooks/useIsLargeAccount'; import { useAllDomainsQuery } from 'src/queries/domains'; +import { useAllFirewallsQuery } from 'src/queries/firewalls'; import { useAllImagesQuery } from 'src/queries/images'; import { useAllKubernetesClustersQuery } from 'src/queries/kubernetes'; import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; @@ -101,6 +102,7 @@ const SearchBar = (props: SearchProps) => { const { data: clusters } = useAllKubernetesClustersQuery(shouldMakeRequests); const { data: volumes } = useAllVolumesQuery({}, {}, shouldMakeRequests); const { data: nodebalancers } = useAllNodeBalancersQuery(shouldMakeRequests); + const { data: firewalls } = useAllFirewallsQuery(shouldMakeRequests); const { data: _privateImages, isLoading: imagesLoading } = useAllImagesQuery( {}, { is_public: false }, // We want to display private images (i.e., not Debian, Ubuntu, etc. distros) @@ -183,7 +185,8 @@ const SearchBar = (props: SearchProps) => { _privateImages ?? [], regions ?? [], searchableLinodes ?? [], - nodebalancers ?? [] + nodebalancers ?? [], + firewalls ?? [] ); } }, [ @@ -198,6 +201,7 @@ const SearchBar = (props: SearchProps) => { _privateImages, regions, nodebalancers, + firewalls, ]); const handleSearchChange = (_searchText: string): void => { @@ -311,9 +315,6 @@ const SearchBar = (props: SearchProps) => { Main search { openMenuOnClick={false} openMenuOnFocus={false} options={finalOptions} + placeholder="Search Products, IP Addresses, Tags..." styles={selectStyles} value={value} /> diff --git a/packages/manager/src/store/selectors/getSearchEntities.ts b/packages/manager/src/store/selectors/getSearchEntities.ts index cdd707c4631..a8f2187a68c 100644 --- a/packages/manager/src/store/selectors/getSearchEntities.ts +++ b/packages/manager/src/store/selectors/getSearchEntities.ts @@ -1,3 +1,4 @@ +import { getFirewallDescription } from 'src/features/Firewalls/shared'; import { getDescriptionForCluster } from 'src/features/Kubernetes/kubeUtils'; import { displayType } from 'src/features/Linodes/presentation'; import { getLinodeDescription } from 'src/utilities/getLinodeDescription'; @@ -5,6 +6,7 @@ import { readableBytes } from 'src/utilities/unitConversions'; import type { Domain, + Firewall, Image, KubernetesCluster, Linode, @@ -162,3 +164,18 @@ export const bucketToSearchableItem = ( label: bucket.label, value: `${bucket.cluster}/${bucket.label}`, }); + +export const firewallToSearchableItem = ( + firewall: Firewall +): SearchableItem => ({ + data: { + created: firewall.created, + description: getFirewallDescription(firewall), + icon: 'firewall', + path: `/firewalls/${firewall.id}`, + tags: firewall.tags, + }, + entityType: 'firewall', + label: firewall.label, + value: firewall.id, +});