From b763fd7474ea660cc436b92cc59b1a982474ed11 Mon Sep 17 00:00:00 2001 From: ricoberger Date: Tue, 30 Mar 2021 18:59:56 +0200 Subject: [PATCH] Use location to load applications Similar to the plugins, we do not load the applications directly anymore after the user clicked the search button. Instead we change the location.search parameter for the applications page. For each change to the location the "fetchApplications" function is triggered to load the applications. This allows a user to share his current selection of clusters and namespaces with other users. --- CHANGELOG.md | 1 + .../components/applications/Applications.tsx | 99 +++++++++++++------ .../applications/ApplicationsToolbar.tsx | 22 ++--- app/src/components/resources/Resources.tsx | 14 +-- 4 files changed, 84 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2e57adfd..d91a3cf93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,3 +29,4 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan - [#7](https://github.com/kobsio/kobs/pull/7): Share datasource options between components and allow sharing of URLs. - [#11](https://github.com/kobsio/kobs/pull/11): :warning: *Breaking change:* :warning: Refactor cluster and application handling. +- [#17](https://github.com/kobsio/kobs/pull/17): Use location to load applications, which allows user to share their applications view. diff --git a/app/src/components/applications/Applications.tsx b/app/src/components/applications/Applications.tsx index ce107fea4..0b1406cb4 100644 --- a/app/src/components/applications/Applications.tsx +++ b/app/src/components/applications/Applications.tsx @@ -9,6 +9,7 @@ import { Title, } from '@patternfly/react-core'; import React, { useCallback, useEffect, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; import { ClustersPromiseClient, GetApplicationsRequest, GetApplicationsResponse } from 'proto/clusters_grpc_web_pb'; import { Application } from 'proto/application_pb'; @@ -18,49 +19,92 @@ import ApplicationsToolbar from 'components/applications/ApplicationsToolbar'; import { apiURL } from 'utils/constants'; import { applicationsDescription } from 'utils/constants'; +// getDataFromSearch returns the clusters and namespaces for the state from a given search location. +export const getDataFromSearch = (search: string): IDataState => { + const params = new URLSearchParams(search); + const clusters = params.getAll('cluster'); + const namespaces = params.getAll('namespace'); + + return { + applications: [], + clusters: clusters, + error: '', + namespaces: namespaces, + }; +}; + // clustersService is the Clusters gRPC service, which is used to get a list of resources. const clustersService = new ClustersPromiseClient(apiURL, null, null); -export interface IScope { +export interface IDataState { + applications: Application.AsObject[]; clusters: string[]; + error: string; namespaces: string[]; } // Applications is the page to display a list of selected applications. To get the applications the user can select a -// scope (list of clusters and namespaces). +// list of clusters and namespaces. const Applications: React.FunctionComponent = () => { - const [scope, setScope] = useState(undefined); - const [applications, setApplications] = useState([]); + const history = useHistory(); + const location = useLocation(); + const [data, setData] = useState(getDataFromSearch(location.search)); const [selectedApplication, setSelectedApplication] = useState(undefined); - const [error, setError] = useState(''); + + // changeData is used to set the provided list of clusters and namespaces as query parameters for the current URL, so + // that a user can share his search with other users. + const changeData = (clusters: string[], namespaces: string[]): void => { + const c = clusters.map((cluster) => `&cluster=${cluster}`); + const n = namespaces.map((namespace) => `&namespace=${namespace}`); + + history.push({ + pathname: location.pathname, + search: `?${c.length > 0 ? c.join('') : ''}${n.length > 0 ? n.join('') : ''}`, + }); + }; // fetchApplications is used to fetch a list of applications. To get the list of applications the user has to select // a list of clusters and namespaces. - const fetchApplications = useCallback(async () => { - if (scope && scope.clusters.length > 0 && scope.namespaces.length > 0) { - try { + const fetchApplications = useCallback(async (d: IDataState) => { + try { + if (d.clusters.length > 0 && d.namespaces.length > 0) { const getApplicationsRequest = new GetApplicationsRequest(); - getApplicationsRequest.setClustersList(scope.clusters); - getApplicationsRequest.setNamespacesList(scope.namespaces); + getApplicationsRequest.setClustersList(d.clusters); + getApplicationsRequest.setNamespacesList(d.namespaces); const getApplicationsResponse: GetApplicationsResponse = await clustersService.getApplications( getApplicationsRequest, null, ); - setApplications(getApplicationsResponse.toObject().applicationsList); - setError(''); - } catch (err) { - setError(err.message); + setData({ + applications: getApplicationsResponse.toObject().applicationsList, + clusters: d.clusters, + error: '', + namespaces: d.namespaces, + }); + } else { + setData({ + applications: [], + clusters: d.clusters, + error: '', + namespaces: d.namespaces, + }); } + } catch (err) { + setData({ + applications: [], + clusters: d.clusters, + error: err.message, + namespaces: d.namespaces, + }); } - }, [scope]); + }, []); - // useEffect is used to call the fetchApplications function every time the list of clusters and namespaces (scope), - // changes. + // useEffect is used to trigger the fetchApplications function, everytime the location.search parameter changes. useEffect(() => { - fetchApplications(); - }, [fetchApplications]); + fetchApplications(getDataFromSearch(location.search)); + }, [location.search, fetchApplications]); return ( @@ -69,7 +113,7 @@ const Applications: React.FunctionComponent = () => { Applications

{applicationsDescription}

- + @@ -85,23 +129,16 @@ const Applications: React.FunctionComponent = () => { > - {!scope ? ( + {data.clusters.length === 0 || data.namespaces.length === 0 ? (

Select a list of clusters and namespaces from the toolbar.

- ) : scope.clusters.length === 0 || scope.namespaces.length === 0 ? ( - -

- You have to select a minimum of one cluster and namespace from the toolbar to search for - applications. -

-
- ) : error ? ( + ) : data.error ? ( -

{error}

+

{data.error}

) : ( - + )}
diff --git a/app/src/components/applications/ApplicationsToolbar.tsx b/app/src/components/applications/ApplicationsToolbar.tsx index 22e2873df..264b62107 100644 --- a/app/src/components/applications/ApplicationsToolbar.tsx +++ b/app/src/components/applications/ApplicationsToolbar.tsx @@ -12,22 +12,27 @@ import FilterIcon from '@patternfly/react-icons/dist/js/icons/filter-icon'; import SearchIcon from '@patternfly/react-icons/dist/js/icons/search-icon'; import { ClustersContext, IClusterContext } from 'context/ClustersContext'; -import { IScope } from 'components/applications/Applications'; import ToolbarItemClusters from 'components/resources/ToolbarItemClusters'; import ToolbarItemNamespaces from 'components/resources/ToolbarItemNamespaces'; interface IApplicationsToolbarProps { - setScope: (scope: IScope) => void; + clusters: string[]; + namespaces: string[]; + changeData: (clusters: string[], namespaces: string[]) => void; } // ApplicationsToolbar is the toolbar, where the user can select a list of clusters and namespaces. When the user clicks // the search button the setScope function is called with the list of selected clusters and namespaces. const ApplicationsToolbar: React.FunctionComponent = ({ - setScope, + clusters, + namespaces, + changeData, }: IApplicationsToolbarProps) => { const clustersContext = useContext(ClustersContext); - const [selectedClusters, setSelectedClusters] = useState([clustersContext.clusters[0]]); - const [selectedNamespaces, setSelectedNamespaces] = useState([]); + const [selectedClusters, setSelectedClusters] = useState( + clusters.length > 0 ? clusters : [clustersContext.clusters[0]], + ); + const [selectedNamespaces, setSelectedNamespaces] = useState(namespaces); // selectCluster adds/removes the given cluster to the list of selected clusters. When the cluster value is an empty // string the selected clusters list is cleared. @@ -80,12 +85,7 @@ const ApplicationsToolbar: React.FunctionComponent = diff --git a/app/src/components/resources/Resources.tsx b/app/src/components/resources/Resources.tsx index 4700d8dfe..87031ca82 100644 --- a/app/src/components/resources/Resources.tsx +++ b/app/src/components/resources/Resources.tsx @@ -49,19 +49,13 @@ const Resources: React.FunctionComponent = () => { > - {!resources ? ( + {!resources || + resources.clusters.length === 0 || + resources.namespaces.length === 0 || + resources.resources.length === 0 ? (

Select a list of clusters, resources and namespaces from the toolbar.

- ) : resources.clusters.length === 0 || - resources.namespaces.length === 0 || - resources.resources.length === 0 ? ( - -

- You have to select a minimum of one cluster, resource and namespace from the toolbar to search for - resources. -

-
) : ( )}