Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
99 changes: 68 additions & 31 deletions app/src/components/applications/Applications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<IScope | undefined>(undefined);
const [applications, setApplications] = useState<Application.AsObject[]>([]);
const history = useHistory();
const location = useLocation();
const [data, setData] = useState<IDataState>(getDataFromSearch(location.search));
const [selectedApplication, setSelectedApplication] = useState<Application.AsObject | undefined>(undefined);
const [error, setError] = useState<string>('');

// 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 (
<React.Fragment>
Expand All @@ -69,7 +113,7 @@ const Applications: React.FunctionComponent = () => {
Applications
</Title>
<p>{applicationsDescription}</p>
<ApplicationsToolbar setScope={setScope} />
<ApplicationsToolbar clusters={data.clusters} namespaces={data.namespaces} changeData={changeData} />
</PageSection>

<Drawer isExpanded={selectedApplication !== undefined}>
Expand All @@ -85,23 +129,16 @@ const Applications: React.FunctionComponent = () => {
>
<DrawerContentBody>
<PageSection style={{ minHeight: '100%' }} variant={PageSectionVariants.default}>
{!scope ? (
{data.clusters.length === 0 || data.namespaces.length === 0 ? (
<Alert variant={AlertVariant.info} title="Select clusters and namespaces">
<p>Select a list of clusters and namespaces from the toolbar.</p>
</Alert>
) : scope.clusters.length === 0 || scope.namespaces.length === 0 ? (
<Alert variant={AlertVariant.danger} title="Select clusters and namespaces">
<p>
You have to select a minimum of one cluster and namespace from the toolbar to search for
applications.
</p>
</Alert>
) : error ? (
) : data.error ? (
<Alert variant={AlertVariant.danger} title="Applications were not fetched">
<p>{error}</p>
<p>{data.error}</p>
</Alert>
) : (
<ApplicationGallery applications={applications} selectApplication={setSelectedApplication} />
<ApplicationGallery applications={data.applications} selectApplication={setSelectedApplication} />
)}
</PageSection>
</DrawerContentBody>
Expand Down
22 changes: 11 additions & 11 deletions app/src/components/applications/ApplicationsToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<IApplicationsToolbarProps> = ({
setScope,
clusters,
namespaces,
changeData,
}: IApplicationsToolbarProps) => {
const clustersContext = useContext<IClusterContext>(ClustersContext);
const [selectedClusters, setSelectedClusters] = useState<string[]>([clustersContext.clusters[0]]);
const [selectedNamespaces, setSelectedNamespaces] = useState<string[]>([]);
const [selectedClusters, setSelectedClusters] = useState<string[]>(
clusters.length > 0 ? clusters : [clustersContext.clusters[0]],
);
const [selectedNamespaces, setSelectedNamespaces] = useState<string[]>(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.
Expand Down Expand Up @@ -80,12 +85,7 @@ const ApplicationsToolbar: React.FunctionComponent<IApplicationsToolbarProps> =
<Button
variant={ButtonVariant.primary}
icon={<SearchIcon />}
onClick={(): void =>
setScope({
clusters: selectedClusters,
namespaces: selectedNamespaces,
})
}
onClick={(): void => changeData(selectedClusters, selectedNamespaces)}
>
Search
</Button>
Expand Down
14 changes: 4 additions & 10 deletions app/src/components/resources/Resources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,13 @@ const Resources: React.FunctionComponent = () => {
>
<DrawerContentBody>
<PageSection style={{ minHeight: '100%' }} variant={PageSectionVariants.default}>
{!resources ? (
{!resources ||
resources.clusters.length === 0 ||
resources.namespaces.length === 0 ||
resources.resources.length === 0 ? (
<Alert variant={AlertVariant.info} title="Select clusters, resources and namespaces">
<p>Select a list of clusters, resources and namespaces from the toolbar.</p>
</Alert>
) : resources.clusters.length === 0 ||
resources.namespaces.length === 0 ||
resources.resources.length === 0 ? (
<Alert variant={AlertVariant.danger} title="Select clusters, resources and namespaces">
<p>
You have to select a minimum of one cluster, resource and namespace from the toolbar to search for
resources.
</p>
</Alert>
) : (
<ResourcesList resources={resources} selectResource={setSelectedResource} />
)}
Expand Down