diff --git a/CHANGELOG.md b/CHANGELOG.md index 91b0481ee..d4621af8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan - [#158](https://github.com/kobsio/kobs/pull/158): Improve Pormetheus HTTP metrics and create a metric to find the most used fields in queries. - [#164](https://github.com/kobsio/kobs/pull/164): Improve chart handling across plugins. - [#167](https://github.com/kobsio/kobs/pull/167): Improve styling across plugins. +- [#169](https://github.com/kobsio/kobs/pull/169): Rework home page to include plugins, applications and teams. ## [v0.5.0](https://github.com/kobsio/kobs/releases/tag/v0.5.0) (2021-08-03) diff --git a/pkg/api/plugins/plugin/plugin.go b/pkg/api/plugins/plugin/plugin.go index 284b2c41d..16f6ab3ad 100644 --- a/pkg/api/plugins/plugin/plugin.go +++ b/pkg/api/plugins/plugin/plugin.go @@ -7,6 +7,7 @@ type Plugin struct { Name string `json:"name"` DisplayName string `json:"displayName"` Description string `json:"description"` + Home bool `json:"home"` Type string `json:"type"` Options map[string]interface{} `json:"options"` } diff --git a/plugins/applications/applications.go b/plugins/applications/applications.go index 4601e8403..d4c7e316f 100644 --- a/plugins/applications/applications.go +++ b/plugins/applications/applications.go @@ -217,6 +217,7 @@ func Register(clusters *clusters.Clusters, plugins *plugin.Plugins, config Confi Name: "applications", DisplayName: "Applications", Description: "Monitor your Kubernetes workloads.", + Home: true, Type: "applications", }) diff --git a/plugins/applications/src/components/home/Home.tsx b/plugins/applications/src/components/home/Home.tsx new file mode 100644 index 000000000..5367fa9be --- /dev/null +++ b/plugins/applications/src/components/home/Home.tsx @@ -0,0 +1,151 @@ +import { + Alert, + AlertActionLink, + AlertVariant, + Badge, + Card, + CardBody, + CardTitle, + Gallery, + GalleryItem, + Spinner, + TextInput, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, +} from '@patternfly/react-core'; +import { QueryObserverResult, useQuery } from 'react-query'; +import React, { useContext, useState } from 'react'; + +import { ClustersContext, IClusterContext, IPluginPageProps, LinkWrapper, useDebounce } from '@kobsio/plugin-core'; +import { IApplication } from '../../utils/interfaces'; + +const Home: React.FunctionComponent = () => { + const [searchTerm, setSearchTerm] = useState(''); + const debouncedSearchTerm = useDebounce(searchTerm, 500); + const clustersContext = useContext(ClustersContext); + + const { isError, isLoading, error, data, refetch } = useQuery( + ['applications/applications', 'gallery', clustersContext.clusters], + async () => { + try { + const clusterParams = clustersContext.clusters.map((cluster) => `cluster=${cluster}`).join('&'); + + const response = await fetch(`/api/plugins/applications/applications?view=gallery&${clusterParams}`, { + method: 'get', + }); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }, + ); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( + + > => refetch()}> + Retry + + + } + > +

{error?.message}

+
+ ); + } + + if (!data) { + return null; + } + + return ( + + + + + + + + + + + + + +

 

+ + + {data + .filter((application) => + !debouncedSearchTerm + ? true + : application.cluster.includes(debouncedSearchTerm) || + application.namespace.includes(debouncedSearchTerm) || + application.name.includes(debouncedSearchTerm) || + application.description?.includes(debouncedSearchTerm), + ) + .map((application, index) => ( + + + + + {application.name} +
+ + {application.namespace} ({application.cluster}) + +
+ +
+ {application.tags && ( +

+ {application.tags.map((tag) => ( + + {tag.toLowerCase()} + + ))} +

+ )} + {application.description &&

{application.description}

} +
+
+
+
+
+ ))} +
+
+ ); +}; + +export default Home; diff --git a/plugins/applications/src/index.ts b/plugins/applications/src/index.ts index be367d3c8..7e7ac0003 100644 --- a/plugins/applications/src/index.ts +++ b/plugins/applications/src/index.ts @@ -4,11 +4,13 @@ import './assets/applications.css'; import icon from './assets/icon.png'; +import Home from './components/home/Home'; import Page from './components/page/Page'; import Panel from './components/panel/Panel'; const applicationsPlugin: IPluginComponents = { applications: { + home: Home, icon: icon, page: Page, panel: Panel, diff --git a/plugins/core/src/components/app/HomePage.tsx b/plugins/core/src/components/app/HomePage.tsx index 328a52a5a..f5e48fa16 100644 --- a/plugins/core/src/components/app/HomePage.tsx +++ b/plugins/core/src/components/app/HomePage.tsx @@ -1,22 +1,93 @@ -import { Gallery, GalleryItem, PageSection, PageSectionVariants } from '@patternfly/react-core'; -import React, { useContext } from 'react'; +import { + Gallery, + GalleryItem, + Grid, + GridItem, + Menu, + MenuContent, + MenuItem, + MenuList, + PageSection, + PageSectionVariants, +} from '@patternfly/react-core'; +import React, { useContext, useEffect, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; import { IPluginsContext, PluginsContext } from '../../context/PluginsContext'; import HomeItem from './HomeItem'; // HomePage renders a list of all registered plugin instances via the HomeItem component. const HomePage: React.FunctionComponent = () => { + const history = useHistory(); + const location = useLocation(); + const pluginsContext = useContext(PluginsContext); + const pluginHome = pluginsContext.getPluginHome(); + const [activePage, setActivePage] = useState('plugins'); + + const pluginDetails = pluginsContext.getPluginDetails(activePage); + const Component = + pluginDetails && pluginsContext.components.hasOwnProperty(pluginDetails.type) + ? pluginsContext.components[pluginDetails.type].home + : undefined; + + const changeActivePage = ( + event?: React.MouseEvent | undefined, + itemId?: string | number | undefined, + ): void => { + if (itemId) { + history.push({ + pathname: location.pathname, + search: `?page=${itemId}`, + }); + } + }; + + useEffect(() => { + const params = new URLSearchParams(location.search); + const page = params.get('page'); + + if (page) { + setActivePage(page); + } + }, [location.search]); return ( - - {pluginsContext.plugins.map((plugin) => ( - - - - ))} - + + + + + + Plugins + {pluginHome.map((plugin, index) => ( + + {plugin.displayName} + + ))} + + + + + + {activePage !== 'plugins' && pluginDetails && Component ? ( + + ) : ( + + {pluginsContext.plugins.map((plugin) => ( + + + + ))} + + )} + + ); }; diff --git a/plugins/core/src/context/PluginsContext.tsx b/plugins/core/src/context/PluginsContext.tsx index 087710aa4..1f7b7cf36 100644 --- a/plugins/core/src/context/PluginsContext.tsx +++ b/plugins/core/src/context/PluginsContext.tsx @@ -28,6 +28,7 @@ export interface IPluginData { name: string; displayName: string; description: string; + home: boolean; type: string; options?: IPluginDataOptions; } @@ -72,6 +73,7 @@ export interface IPluginPreviewProps { // IPluginComponent is the interface which must be implemented by each plugin. It must contain an icon and panel // component. The page and preview component is optional for each plugin. export interface IPluginComponent { + home?: React.FunctionComponent; icon: string; page?: React.FunctionComponent; panel: React.FunctionComponent; @@ -88,6 +90,7 @@ export interface IPluginComponents { export interface IPluginsContext { components: IPluginComponents; getPluginDetails: (name: string) => IPluginData | undefined; + getPluginHome: () => IPluginData[]; getPluginIcon: (type: string) => string; plugins: IPluginData[]; } @@ -98,6 +101,9 @@ export const PluginsContext = React.createContext({ getPluginDetails: (name: string) => { return undefined; }, + getPluginHome: () => { + return []; + }, getPluginIcon: (type: string) => { return ''; }, @@ -161,6 +167,11 @@ export const PluginsContextProvider: React.FunctionComponent { + const pluginHome = data?.filter((p) => p.home); + return pluginHome || []; + }; + const getPluginIcon = (type: string): string => { if (components.hasOwnProperty(type)) { return components[type].icon; @@ -206,6 +217,7 @@ export const PluginsContextProvider: React.FunctionComponent = () => { + const [searchTerm, setSearchTerm] = useState(''); + const debouncedSearchTerm = useDebounce(searchTerm, 500); + + const { isError, isLoading, error, data, refetch } = useQuery(['teams/teams'], async () => { + try { + const response = await fetch(`/api/plugins/teams/teams`, { method: 'get' }); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( + + > => refetch()}> + Retry + + + } + > +

{error?.message}

+
+ ); + } + + if (!data) { + return null; + } + + return ( + + + + + + + + + + + + + +

 

+ + + {data + .filter((team) => + !debouncedSearchTerm + ? true + : team.cluster.includes(debouncedSearchTerm) || + team.namespace.includes(debouncedSearchTerm) || + team.name.includes(debouncedSearchTerm) || + team.description?.includes(debouncedSearchTerm), + ) + .map((team, index) => ( + + + + ))} + +
+ ); +}; + +export default Home; diff --git a/plugins/teams/src/index.ts b/plugins/teams/src/index.ts index ea9ae0385..da02f1947 100644 --- a/plugins/teams/src/index.ts +++ b/plugins/teams/src/index.ts @@ -2,11 +2,13 @@ import { IPluginComponents } from '@kobsio/plugin-core'; import icon from './assets/icon.png'; +import Home from './components/home/Home'; import Page from './components/page/Page'; import Panel from './components/panel/Panel'; const teamsPlugin: IPluginComponents = { teams: { + home: Home, icon: icon, page: Page, panel: Panel, diff --git a/plugins/teams/teams.go b/plugins/teams/teams.go index 4904ad28f..f77a49fed 100644 --- a/plugins/teams/teams.go +++ b/plugins/teams/teams.go @@ -82,6 +82,7 @@ func Register(clusters *clusters.Clusters, plugins *plugin.Plugins, config Confi Name: "teams", DisplayName: "Teams", Description: "Define an ownership for your Kubernetes resources.", + Home: true, Type: "teams", })