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. -

-
) : ( )}