diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bcb5cebf..450f0f2a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -NOTE: As semantic versioning states all 0.y.z releases can contain breaking changes in API (flags, grpc API, any backward compatibility). We use :warning: Breaking change :warning: to mark changes that are not backward compatible (relates only to v0.y.z releases). +NOTE: As semantic versioning states all 0.y.z releases can contain breaking changes in API (flags, grpc API, any backward compatibility). We use :warning: *Breaking change:* :warning: to mark changes that are not backward compatible (relates only to v0.y.z releases). ## Unreleased @@ -23,3 +23,4 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan ### Changed - [#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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be1271813..5c02a42d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ The following section explains various suggestions and procedures to note during ### Prerequisites - It is strongly recommended that you use macOS or Linux distributions for development. -- You have Go 1.15.0 or newer installed. +- You have Go 1.16.0 or newer installed. - You have Node.js 14.0.0 or newer installed. - For the React UI, you will need a working NodeJS environment and the Yarn package manager to compile the Web UI assets. diff --git a/Makefile b/Makefile index ace03e2b7..c5b7b37e2 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,8 @@ REPO ?= github.com/kobsio/kobs REVISION ?= $(shell git rev-parse HEAD) VERSION ?= $(shell git describe --tags) +PLUGINS ?= $(shell find ./proto -name '*.proto' | sed -e 's/^.\/proto\///' | sed -e 's/.proto//') + .PHONY: build build: @go build -ldflags "-X ${REPO}/pkg/version.Version=${VERSION} \ @@ -19,28 +21,26 @@ generate: generate-proto generate-crd .PHONY: generate-proto generate-proto: - @protoc --proto_path=proto --go_out=pkg/generated/proto --go_opt=paths=source_relative --go-grpc_out=pkg/generated/proto --go-grpc_opt=paths=source_relative --deepcopy_out=pkg/generated/proto --js_out=import_style=commonjs:app/src/generated/proto --plugin=protoc-gen-ts=app/node_modules/.bin/protoc-gen-ts --ts_out=service=grpc-web:app/src/generated/proto --grpc-web_out=import_style=commonjs,mode=grpcwebtext:app/src/generated/proto proto/clusters.proto - @protoc --proto_path=proto --go_out=pkg/generated/proto --go_opt=paths=source_relative --go-grpc_out=pkg/generated/proto --go-grpc_opt=paths=source_relative --deepcopy_out=pkg/generated/proto --js_out=import_style=commonjs:app/src/generated/proto --plugin=protoc-gen-ts=app/node_modules/.bin/protoc-gen-ts --ts_out=service=grpc-web:app/src/generated/proto --grpc-web_out=import_style=commonjs,mode=grpcwebtext:app/src/generated/proto proto/datasources.proto - @protoc --proto_path=proto --go_out=pkg/generated/proto --go_opt=paths=source_relative --go-grpc_out=pkg/generated/proto --go-grpc_opt=paths=source_relative --deepcopy_out=pkg/generated/proto --js_out=import_style=commonjs:app/src/generated/proto --plugin=protoc-gen-ts=app/node_modules/.bin/protoc-gen-ts --ts_out=service=grpc-web:app/src/generated/proto --grpc-web_out=import_style=commonjs,mode=grpcwebtext:app/src/generated/proto proto/application.proto - @rm -rf ./pkg/generated/proto/clusters_deepcopy.gen.go - @rm -rf ./pkg/generated/proto/datasources_deepcopy.gen.go - @rm -rf ./pkg/generated/proto/application_deepcopy.gen.go - @mv ./pkg/generated/proto/github.com/kobsio/kobs/pkg/generated/proto/clusters_deepcopy.gen.go ./pkg/generated/proto - @mv ./pkg/generated/proto/github.com/kobsio/kobs/pkg/generated/proto/datasources_deepcopy.gen.go ./pkg/generated/proto - @mv ./pkg/generated/proto/github.com/kobsio/kobs/pkg/generated/proto/application_deepcopy.gen.go ./pkg/generated/proto - @rm -rf ./pkg/generated/proto/github.com + for plugin in $(PLUGINS); do \ + mkdir -p pkg/api/plugins/$$plugin/proto; \ + mkdir -p app/src/proto; \ + protoc --proto_path=proto --go_out=pkg/api/plugins/$$plugin/proto --go_opt=paths=source_relative --go-grpc_out=pkg/api/plugins/$$plugin/proto --go-grpc_opt=paths=source_relative --deepcopy_out=pkg/api/plugins/$$plugin/proto --js_out=import_style=commonjs:app/src/proto --plugin=protoc-gen-ts=app/node_modules/.bin/protoc-gen-ts --ts_out=service=grpc-web:app/src/proto --grpc-web_out=import_style=commonjs,mode=grpcwebtext:app/src/proto proto/$$plugin.proto; \ + rm -rf ./pkg/api/plugins/$$plugin/proto/$$plugin\_deepcopy.gen.go; \ + mv ./pkg/api/plugins/$$plugin/proto/github.com/kobsio/kobs/pkg/api/plugins/$$plugin/proto/$$plugin\_deepcopy.gen.go ./pkg/api/plugins/$$plugin/proto; \ + rm -rf ./pkg/api/plugins/$$plugin/proto/github.com; \ + done .PHONY: generate-crd generate-crd: - @${GOPATH}/src/k8s.io/code-generator/generate-groups.sh "deepcopy,client,informer,lister" github.com/kobsio/kobs/pkg/generated github.com/kobsio/kobs/pkg/api/kubernetes/apis application:v1alpha1 --output-base ./tmp - @rm -rf ./pkg/api/kubernetes/apis/application/v1alpha1/zz_generated.deepcopy.go - @rm -rf ./pkg/generated/clientset - @rm -rf ./pkg/generated/informers - @rm -rf ./pkg/generated/listers - @mv ./tmp/github.com/kobsio/kobs/pkg/api/kubernetes/apis/application/v1alpha1/zz_generated.deepcopy.go ./pkg/api/kubernetes/apis/application/v1alpha1 - @mv ./tmp/github.com/kobsio/kobs/pkg/generated/clientset ./pkg/generated/clientset - @mv ./tmp/github.com/kobsio/kobs/pkg/generated/informers ./pkg/generated/informers - @mv ./tmp/github.com/kobsio/kobs/pkg/generated/listers ./pkg/generated/listers + @${GOPATH}/src/k8s.io/code-generator/generate-groups.sh "deepcopy,client,informer,lister" github.com/kobsio/kobs/pkg/api/plugins/application github.com/kobsio/kobs/pkg/api/plugins/application/apis application:v1alpha1 --output-base ./tmp + @rm -rf ./pkg/api/plugins/application/apis/application/v1alpha1/zz_generated.deepcopy.go + @rm -rf ./pkg/api/plugins/application/clientset + @rm -rf ./pkg/api/plugins/application/informers + @rm -rf ./pkg/api/plugins/application/listers + @mv ./tmp/github.com/kobsio/kobs/pkg/api/plugins/application/apis/application/v1alpha1/zz_generated.deepcopy.go ./pkg/api/plugins/application/apis/application/v1alpha1 + @mv ./tmp/github.com/kobsio/kobs/pkg/api/plugins/application/clientset ./pkg/api/plugins/application/clientset + @mv ./tmp/github.com/kobsio/kobs/pkg/api/plugins/application/informers ./pkg/api/plugins/application/informers + @mv ./tmp/github.com/kobsio/kobs/pkg/api/plugins/application/listers ./pkg/api/plugins/application/listers @rm -rf ./tmp -controller-gen "crd:trivialVersions=true" paths="./..." output:crd:artifacts:config=deploy/kustomize/crds diff --git a/app/.eslintignore b/app/.eslintignore index c83f90a2b..4c3c29bf7 100644 --- a/app/.eslintignore +++ b/app/.eslintignore @@ -1 +1 @@ -src/generated +src/proto diff --git a/app/package.json b/app/package.json index 5e1c51df7..0959d7677 100644 --- a/app/package.json +++ b/app/package.json @@ -146,16 +146,16 @@ "@patternfly/react-core": "^4.90.2", "@patternfly/react-table": "^4.20.15", "@types/google-protobuf": "^3.7.4", - "@types/highlight.js": "^10.1.0", "@types/node": "^12.0.0", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@types/react-router-dom": "^5.1.7", "google-protobuf": "^3.15.0-rc.1", "grpc-web": "^1.2.1", - "highlight.js": "^10.6.0", "js-yaml": "^4.0.0", + "jsonpath-plus": "^5.0.4", "react": "^17.0.1", + "react-ace": "^9.3.0", "react-dom": "^17.0.1", "react-router-dom": "^5.2.0", "react-scripts": "4.0.2", diff --git a/app/public/img/datasources/jaeger.png b/app/public/img/datasources/jaeger.png new file mode 100644 index 000000000..a4a6d23b6 Binary files /dev/null and b/app/public/img/datasources/jaeger.png differ diff --git a/app/src/App.tsx b/app/src/App.tsx index e45622650..715b14872 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,5 +1,5 @@ -import { Page, PageHeader } from '@patternfly/react-core'; -import { Route, BrowserRouter as Router, Switch } from 'react-router-dom'; +import { Brand, Page, PageHeader } from '@patternfly/react-core'; +import { Link, Route, BrowserRouter as Router, Switch } from 'react-router-dom'; import React from 'react'; import '@patternfly/react-core/dist/styles/base.css'; @@ -9,31 +9,31 @@ import '@patternfly/patternfly/patternfly-charts.css'; import Application from 'components/applications/Application'; import Applications from 'components/applications/Applications'; -import Datasource from 'components/datasources/Datasource'; -import Datasources from 'components/datasources/Datasources'; -import HeaderLogo from 'components/shared/HeaderLogo'; -import Overview from 'components/overview/Overview'; +import { ClustersContextProvider } from 'context/ClustersContext'; +import Home from 'components/Home'; import Resources from 'components/resources/Resources'; import 'app.css'; // App is used to set all routes for the react-router and the header for all pages. const App: React.FunctionComponent = () => { - const Header = } />; + const Header = ( + } /> + ); return ( - - - - - - - - - - - - + + + + + + + + + + + + ); }; diff --git a/app/src/app.css b/app/src/app.css index ba50b76b8..f9345dbe2 100644 --- a/app/src/app.css +++ b/app/src/app.css @@ -4,121 +4,3 @@ #root { height: 100%; } - -/* kobs-home-item - * The items on the home page are clickable, so we have to set the correct cursor style */ -.kobs-home-item { - cursor: pointer; -} - -/* kobs-drawer-pagesection - * When we use a PageSection component within a Drawer component, we have to explicit set the height of the PageSection - * to 100%. */ -.kobs-drawer-pagesection { - min-height: 100%; -} - -/* kobs-drawer-actions - * Customize the drawer actions. For example we remove the padding from the action buttons, so that they are displayed - * in one line with the drawer title. */ -.kobs-drawer-actions button { - padding: 0; -} - -/* kobs-drawer-panel-body - * Is used to modify the style of the drawer panel body. For example we have to set the maximum width for tabs used - * within the drawer and apply some margin for the top and bottom of the tabs content. */ -.kobs-drawer-panel-body .pf-c-tab-content { - margin: 16px 0px; - max-width: 100%; - overflow-x: scroll; -} - -/* kobsio-pagesection-toolbar - * Is used to display the toolbar within a page section. For that we have to adjust the z-index, so that the toolbar is - * always displayed above the other components. We also remove the bottom, because the padding is already applied by the - * PageSection component. - * We also remove the padding for the ToolbarContent component, so that the ToolbarItems are aligned with the other - * content in the PageSection. */ -.kobsio-pagesection-toolbar { - padding-bottom: 0px; - z-index: 300; -} - -.kobsio-pagesection-toolbar .pf-c-toolbar__content { - padding: 0px; -} - -/* kobsio-pagesection-tabs - * This is used if we display tabs at the bottom of a PageSection component, to remove the padding. This allows us to - * align the tabs line with the end of the PageSection. */ -.kobsio-pagesection-tabs { - padding-bottom: 0px; -} - -/* kobs-fullwidth - * We often need to modify the width of elements, so that they are taken the full width of the parent. For example to - * display an item in the toolbar, which is aligned on the right side. For that the following class can be used. */ -.kobs-fullwidth { - width: 100%; -} - -/* kobsio-options-list - * Do not center the items vertically and apply some padding between the list items for smaller screens. */ -.kobsio-options-list { - align-items: flex-start; -} - -.kobsio-options-list-item { - padding-bottom: 16px; -} - -/* kobsio-charts-grid - * Apply some marging on the top of the charts grid. */ -.kobsio-charts-grid { - margin-top: 16px; -} - -/* kobsio-chart-container - * Set the width and height for a chart. The width and height is applied to the chart container and then used via - * useRef within the chart. */ -.kobsio-chart-container-default { - width: 100%; - height: 300px; -} - -.kobsio-chart-container-default-small { - width: 100%; - height: 200px; -} - -.kobsio-chart-container-sparkline { - width: 100%; - height: 150px; - position: relative; -} - -.kobsio-chart-container-sparkline.small { - height: 75px; -} - -.kobsio-chart-container-sparkline-value { - width: 100%; - top: 63px; - font-size: 24px; - position: absolute; - text-align: center -} - -/* kobsis-table-wrapper - * Wrap a table component, so it looks nice within a page, but allow scrolling so the user can see all the data. */ -.kobsis-table-wrapper { - max-width: 100%; - overflow-x: scroll; -} - -/* kobsio-tab-content - * Set a min height of 100% for the tab content. */ -.kobsio-tab-content { - min-height: 100%; -} diff --git a/app/src/components/Editor.tsx b/app/src/components/Editor.tsx new file mode 100644 index 000000000..e4f6b2bb6 --- /dev/null +++ b/app/src/components/Editor.tsx @@ -0,0 +1,48 @@ +import React, { useRef } from 'react'; +import AceEditor from 'react-ace'; + +import 'ace-builds/src-noconflict/mode-yaml'; +import 'ace-builds/src-noconflict/theme-nord_dark'; + +// IEditorProps is the interface for the Editor props. The editor requires a value, which is shown in the Editor field, +// a mode, which defines the language. If the editor isn't set to read only the user can also pass in a onChange +// function to retrieve the changes from the editor. +interface IEditorProps { + value: string; + mode: string; + readOnly: boolean; + onChange?: (newValue: string) => void; +} + +// Editor is the editor component, which can be used to show for example the yaml representation of a resource. +const Editor: React.FunctionComponent = ({ value, mode, readOnly, onChange }: IEditorProps) => { + const editor = useRef(null); + + const changeValue = (newValue: string): void => { + if (onChange) { + onChange(newValue); + } + }; + + return ( + + ); +}; + +export default Editor; diff --git a/app/src/components/Home.tsx b/app/src/components/Home.tsx new file mode 100644 index 000000000..611ac6176 --- /dev/null +++ b/app/src/components/Home.tsx @@ -0,0 +1,26 @@ +import { Gallery, GalleryItem, PageSection, PageSectionVariants } from '@patternfly/react-core'; +import React from 'react'; + +import { applicationsDescription, resourcesDescription } from 'utils/constants'; +import HomeItem from 'components/HomeItem'; + +// Home is the root component of kobs. It is used to render a list of pages, which can be used by the user. The items +// which are displayed to the user are the applications and resources page and a list of all configured plugins. +// The items for the gallery should always use the HomeItem component, this will render a card, which are selectable. By +// a click on the item the user is navigated to the corresponding page. +const Home: React.FunctionComponent = () => { + return ( + + + + + + + + + + + ); +}; + +export default Home; diff --git a/app/src/components/HomeItem.tsx b/app/src/components/HomeItem.tsx new file mode 100644 index 000000000..d5b93d587 --- /dev/null +++ b/app/src/components/HomeItem.tsx @@ -0,0 +1,29 @@ +import { Card, CardBody, CardTitle } from '@patternfly/react-core'; +import React from 'react'; +import { useHistory } from 'react-router-dom'; + +// IHomeItemProps is the interface for an item on the home page. Each item contains a title, body and a link. +interface IHomeItemProps { + body: string; + link: string; + title: string; +} + +// HomeItem is used to render an item in the home page. It requires a title, body and a link. When the card is clicked, +// the user is redirected to the provided link. +const HomeItem: React.FunctionComponent = ({ body, link, title }: IHomeItemProps) => { + const history = useHistory(); + + const handleClick = (): void => { + history.push(link); + }; + + return ( + + {title} + {body} + + ); +}; + +export default HomeItem; diff --git a/app/src/components/shared/Title.tsx b/app/src/components/Title.tsx similarity index 73% rename from app/src/components/shared/Title.tsx rename to app/src/components/Title.tsx index e49a86df9..b98d1f6b9 100644 --- a/app/src/components/shared/Title.tsx +++ b/app/src/components/Title.tsx @@ -1,7 +1,10 @@ import React from 'react'; +// TTitleSize are is the size type. We support the lg and xl property of Patternfly for the text size of the title. type TTitleSize = 'lg' | 'xl'; +// ITitleProps are the properties for the Title component. The user must provide the title, subtitle and the size for +// the title. interface ITitleProps { title: string; subtitle: string; diff --git a/app/src/components/applications/Application.tsx b/app/src/components/applications/Application.tsx index d381c1cd0..bd9f5100e 100644 --- a/app/src/components/applications/Application.tsx +++ b/app/src/components/applications/Application.tsx @@ -7,62 +7,57 @@ import { ListVariant, PageSection, PageSectionVariants, + Spinner, } from '@patternfly/react-core'; -import { Link, useHistory, useParams } from 'react-router-dom'; import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; -import { GetApplicationRequest, GetApplicationResponse } from 'generated/proto/clusters_pb'; -import Tabs, { DEFAULT_TAB } from 'components/applications/details/Tabs'; -import { Application } from 'generated/proto/application_pb'; -import { ClustersPromiseClient } from 'generated/proto/clusters_grpc_web_pb'; -import TabsContent from 'components/applications/details/TabsContent'; -import Title from 'components/shared/Title'; +import { ClustersPromiseClient, GetApplicationRequest, GetApplicationResponse } from 'proto/clusters_grpc_web_pb'; +import ApplicationTabs from 'components/applications/ApplicationTabs'; +import ApplicationTabsContent from 'components/applications/ApplicationTabsContent'; +import { Application as IApplication } from 'proto/application_pb'; +import Title from 'components/Title'; import { apiURL } from 'utils/constants'; -const clustersService = new ClustersPromiseClient(apiURL, null, null); +interface IDataState { + application?: IApplication.AsObject; + error: string; + isLoading: boolean; +} interface IApplicationsParams { cluster: string; namespace: string; name: string; } +// clustersService is the Clusters gRPC service, which is used to get an application. +const clustersService = new ClustersPromiseClient(apiURL, null, null); -// Applications is the page component to show a single application. The application is determined by the provided page -// params. When the application could not be loaded an error is shown. If the application was successfully loaded, we -// show the same components as in the tabs from the Applications drawer. -const Applications: React.FunctionComponent = () => { +const Application: React.FunctionComponent = () => { const history = useHistory(); const params = useParams(); - const [application, setApplication] = useState(undefined); - const [error, setError] = useState(''); + const [data, setData] = useState({ application: undefined, error: '', isLoading: true }); - const [tab, setTab] = useState(DEFAULT_TAB); + const [activeTab, setActiveTab] = useState('resources'); const refResourcesContent = useRef(null); - const refMetricsContent = useRef(null); - const refLogsContent = useRef(null); - const goToOverview = (): void => { - history.push('/'); - }; - - // fetchApplication is used to fetch the application from the gRPC API. This is done every time the page paramertes - // change. When there is an error during the fetch, the user will see the error. const fetchApplication = useCallback(async () => { try { + setData({ application: undefined, error: '', isLoading: true }); + const getApplicationRequest = new GetApplicationRequest(); getApplicationRequest.setCluster(params.cluster); getApplicationRequest.setNamespace(params.namespace); getApplicationRequest.setName(params.name); - const getApplicationsResponse: GetApplicationResponse = await clustersService.getApplication( + const getApplicationResponse: GetApplicationResponse = await clustersService.getApplication( getApplicationRequest, null, ); - setError(''); - setApplication(getApplicationsResponse.getApplication()); + setData({ application: getApplicationResponse.toObject().application, error: '', isLoading: false }); } catch (err) { - setError(err.message); + setData({ application: undefined, error: err.message, isLoading: false }); } }, [params.cluster, params.namespace, params.name]); @@ -70,69 +65,61 @@ const Applications: React.FunctionComponent = () => { fetchApplication(); }, [fetchApplication]); - if (!application) { - return null; + if (data.isLoading) { + return ; } - // If there is an error, we will show it to the user. The user then has the option to retry the failed API call or to - // go to the overview page. - if (error) { + if (data.error || !data.application) { return ( - - - Retry - Overview - - } - > -

{error}

-
-
+ + history.push('/')}>Home + Retry + + } + > +

{data.error ? data.error : 'Application is undefined'}

+
); } return ( - + - <List variant={ListVariant.inline}> - {application.getLinksList().map((link, index) => ( - <ListItem key={index}> - <Link target="_blank" to={link.getLink}> - {link.getTitle()} - </Link> - </ListItem> - ))} - </List> - <Tabs - tab={tab} - setTab={(t: string): void => setTab(t)} - refResourcesContent={refResourcesContent} - refMetricsContent={refMetricsContent} - refLogsContent={refLogsContent} - /> + {data.application.details ? ( + <div> + <p>{data.application.details.description}</p> + <List variant={ListVariant.inline}> + {data.application.details.linksList.map((link, index) => ( + <ListItem key={index}> + <a href={link.link} rel="noreferrer" target="_blank"> + {link.title} + </a> + </ListItem> + ))} + </List> + </div> + ) : null} + <ApplicationTabs activeTab={activeTab} setTab={setActiveTab} refResourcesContent={refResourcesContent} /> </PageSection> - <PageSection variant={PageSectionVariants.default}> - <TabsContent - application={application} - tab={tab} - refResourcesContent={refResourcesContent} - refMetricsContent={refMetricsContent} - refLogsContent={refLogsContent} - /> - </PageSection> + <ApplicationTabsContent + application={data.application} + activeTab={activeTab} + isInDrawer={false} + refResourcesContent={refResourcesContent} + /> </React.Fragment> ); }; -export default Applications; +export default Application; diff --git a/app/src/components/applications/ApplicationDetails.tsx b/app/src/components/applications/ApplicationDetails.tsx new file mode 100644 index 000000000..1f8063b79 --- /dev/null +++ b/app/src/components/applications/ApplicationDetails.tsx @@ -0,0 +1,72 @@ +import { + DrawerActions, + DrawerCloseButton, + DrawerHead, + DrawerPanelBody, + DrawerPanelContent, + List, + ListItem, + ListVariant, +} from '@patternfly/react-core'; +import React, { useRef, useState } from 'react'; + +import { Application } from 'proto/application_pb'; +import ApplicationDetailsLink from 'components/applications/ApplicationDetailsLink'; +import ApplicationTabs from 'components/applications/ApplicationTabs'; +import ApplicationTabsContent from 'components/applications/ApplicationTabsContent'; +import Title from 'components/Title'; + +interface IApplicationDetailsProps { + application: Application.AsObject; + close: () => void; +} + +// ApplicationDetails is the details view of an application, which is displayed as a drawer panel. +const ApplicationDetails: React.FunctionComponent<IApplicationDetailsProps> = ({ + application, + close, +}: IApplicationDetailsProps) => { + const [activeTab, setActiveTab] = useState<string>('resources'); + const refResourcesContent = useRef<HTMLElement>(null); + + return ( + <DrawerPanelContent minSize="50%"> + <DrawerHead> + <Title title={application.name} subtitle={`${application.namespace} (${application.cluster})`} size="lg" /> + <DrawerActions style={{ padding: 0 }}> + <DrawerCloseButton onClose={close} /> + </DrawerActions> + </DrawerHead> + + <DrawerPanelBody> + {application.details ? ( + <div> + <p>{application.details.description}</p> + + <List variant={ListVariant.inline}> + <ListItem> + <ApplicationDetailsLink application={application} /> + </ListItem> + {application.details.linksList.map((link, index) => ( + <ListItem key={index}> + <a href={link.link} rel="noreferrer" target="_blank"> + {link.title} + </a> + </ListItem> + ))} + </List> + </div> + ) : null} + <ApplicationTabs activeTab={activeTab} setTab={setActiveTab} refResourcesContent={refResourcesContent} /> + <ApplicationTabsContent + application={application} + activeTab={activeTab} + isInDrawer={true} + refResourcesContent={refResourcesContent} + /> + </DrawerPanelBody> + </DrawerPanelContent> + ); +}; + +export default ApplicationDetails; diff --git a/app/src/components/applications/ApplicationDetailsLink.tsx b/app/src/components/applications/ApplicationDetailsLink.tsx new file mode 100644 index 000000000..6a2c66858 --- /dev/null +++ b/app/src/components/applications/ApplicationDetailsLink.tsx @@ -0,0 +1,29 @@ +import { Link, useLocation } from 'react-router-dom'; +import React, { useEffect, useState } from 'react'; + +import { Application } from 'proto/application_pb'; + +interface IApplicationDetailsLinkProps { + application: Application.AsObject; +} + +// ApplicationDetailsLink renders the link to the details page for an application inside the DrawerPanel of the +// applications page. Everytime when the location.search parameter (query parameters) are changing, we are adding the +// new parameters to the link, so that for example a change of the selected time range is also used in the details page. +const ApplicationDetailsLink: React.FunctionComponent<IApplicationDetailsLinkProps> = ({ + application, +}: IApplicationDetailsLinkProps) => { + const location = useLocation(); + + const [link, setLink] = useState<string>( + `/applications/${application.cluster}/${application.namespace}/${application.name}`, + ); + + useEffect(() => { + setLink(`/applications/${application.cluster}/${application.namespace}/${application.name}${location.search}`); + }, [application, location.search]); + + return <Link to={link}>Details</Link>; +}; + +export default ApplicationDetailsLink; diff --git a/app/src/components/applications/ApplicationGallery.tsx b/app/src/components/applications/ApplicationGallery.tsx new file mode 100644 index 000000000..732dea833 --- /dev/null +++ b/app/src/components/applications/ApplicationGallery.tsx @@ -0,0 +1,28 @@ +import { Gallery, GalleryItem } from '@patternfly/react-core'; +import React from 'react'; + +import { Application } from 'proto/application_pb'; +import ApplicationItem from 'components/applications/ApplicationItem'; + +interface IApplicationGalleryProps { + applications: Application.AsObject[]; + selectApplication: (application: Application.AsObject) => void; +} + +// ApplicationGallery is the component to display all applications inside a gallery view. +const ApplicationGallery: React.FunctionComponent<IApplicationGalleryProps> = ({ + applications, + selectApplication, +}: IApplicationGalleryProps) => { + return ( + <Gallery hasGutter={true}> + {applications.map((application, index) => ( + <GalleryItem key={index}> + <ApplicationItem application={application} selectApplication={selectApplication} /> + </GalleryItem> + ))} + </Gallery> + ); +}; + +export default ApplicationGallery; diff --git a/app/src/components/applications/ApplicationItem.tsx b/app/src/components/applications/ApplicationItem.tsx new file mode 100644 index 000000000..4d6eeeaee --- /dev/null +++ b/app/src/components/applications/ApplicationItem.tsx @@ -0,0 +1,28 @@ +import { Card, CardBody, CardTitle } from '@patternfly/react-core'; +import React from 'react'; + +import { Application } from 'proto/application_pb'; + +interface IApplicationItemProps { + application: Application.AsObject; + selectApplication: (application: Application.AsObject) => void; +} + +// ApplicationItem renders a single application in a Card component. With the title of the application and the +// description of the application. If the user doesn't provide a description, we just show the namespace and cluster of +// the application in the card body. +const ApplicationItem: React.FunctionComponent<IApplicationItemProps> = ({ + application, + selectApplication, +}: IApplicationItemProps) => { + return ( + <Card style={{ cursor: 'pointer' }} isHoverable={true} onClick={(): void => selectApplication(application)}> + <CardTitle>{application.name}</CardTitle> + <CardBody> + {application.details ? application.details.description : `${application.namespace} (${application.cluster})`} + </CardBody> + </Card> + ); +}; + +export default ApplicationItem; diff --git a/app/src/components/applications/ApplicationTabs.tsx b/app/src/components/applications/ApplicationTabs.tsx new file mode 100644 index 000000000..6382c831d --- /dev/null +++ b/app/src/components/applications/ApplicationTabs.tsx @@ -0,0 +1,34 @@ +import { Tab, TabTitleText, Tabs } from '@patternfly/react-core'; +import React from 'react'; + +interface IApplicationTabsParams { + activeTab: string; + setTab(tab: string): void; + refResourcesContent: React.RefObject<HTMLElement>; +} + +// ApplicationTabs is the component to render all tabs for an application. An application always contains a tab for +// resources and a dynamic list of plugins. +const ApplicationTabs: React.FunctionComponent<IApplicationTabsParams> = ({ + activeTab, + setTab, + refResourcesContent, +}: IApplicationTabsParams) => { + return ( + <Tabs + className="pf-u-mt-md" + isFilled={true} + activeKey={activeTab} + onSelect={(event, tabIndex): void => setTab(tabIndex.toString())} + > + <Tab + eventKey="resources" + title={<TabTitleText>Resources</TabTitleText>} + tabContentId="refResources" + tabContentRef={refResourcesContent} + /> + </Tabs> + ); +}; + +export default ApplicationTabs; diff --git a/app/src/components/applications/ApplicationTabsContent.tsx b/app/src/components/applications/ApplicationTabsContent.tsx new file mode 100644 index 000000000..3267ceee0 --- /dev/null +++ b/app/src/components/applications/ApplicationTabsContent.tsx @@ -0,0 +1,71 @@ +import { + Drawer, + DrawerContent, + DrawerContentBody, + PageSection, + PageSectionVariants, + TabContent, +} from '@patternfly/react-core'; +import React, { useState } from 'react'; +import { IRow } from '@patternfly/react-table'; + +import { Application } from 'proto/application_pb'; +import ResourceDetails from 'components/resources/ResourceDetails'; +import ResourcesList from 'components/resources/ResourcesList'; + +interface IApplicationTabsContent { + application: Application.AsObject; + activeTab: string; + isInDrawer: boolean; + refResourcesContent: React.RefObject<HTMLElement>; +} + +// ApplicationTabsContent renders the content of an tab. If the component isn't rendered inside a drawer it provides a +// drawer, so the a component in the tab content can display some details in his own drawer. +const ApplicationTabsContent: React.FunctionComponent<IApplicationTabsContent> = ({ + application, + activeTab, + isInDrawer, + refResourcesContent, +}: IApplicationTabsContent) => { + const [panelContent, setPanelContent] = useState<React.ReactNode | undefined>(undefined); + + return ( + <Drawer isExpanded={panelContent !== undefined}> + <DrawerContent panelContent={panelContent}> + <DrawerContentBody> + <PageSection + style={isInDrawer ? { minHeight: '100%', paddingLeft: '0px', paddingRight: '0px' } : { minHeight: '100%' }} + variant={isInDrawer ? PageSectionVariants.light : PageSectionVariants.default} + > + <TabContent + style={{ minHeight: '100%' }} + eventKey="resources" + id="refResources" + activeKey={activeTab} + ref={refResourcesContent} + aria-label="Resources" + > + <ResourcesList + resources={{ + clusters: [application.cluster], + namespaces: [application.namespace], + resources: application.resourcesList, + }} + selectResource={(resource: IRow): void => + isInDrawer + ? setPanelContent(undefined) + : setPanelContent( + <ResourceDetails resource={resource} close={(): void => setPanelContent(undefined)} />, + ) + } + /> + </TabContent> + </PageSection> + </DrawerContentBody> + </DrawerContent> + </Drawer> + ); +}; + +export default ApplicationTabsContent; diff --git a/app/src/components/applications/Applications.tsx b/app/src/components/applications/Applications.tsx index adf2420b3..ce107fea4 100644 --- a/app/src/components/applications/Applications.tsx +++ b/app/src/components/applications/Applications.tsx @@ -4,68 +4,63 @@ import { Drawer, DrawerContent, DrawerContentBody, - Gallery, - GalleryItem, PageSection, PageSectionVariants, Title, } from '@patternfly/react-core'; -import React, { useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; -import { GetApplicationsRequest, GetApplicationsResponse } from 'generated/proto/clusters_pb'; -import { apiURL, applicationsDescription } from 'utils/constants'; -import { Application } from 'generated/proto/application_pb'; -import Card from 'components/applications/overview/Card'; -import { ClustersPromiseClient } from 'generated/proto/clusters_grpc_web_pb'; -import DrawerPanel from 'components/applications/details/DrawerPanel'; -import Filter from 'components/resources/shared/Filter'; +import { ClustersPromiseClient, GetApplicationsRequest, GetApplicationsResponse } from 'proto/clusters_grpc_web_pb'; +import { Application } from 'proto/application_pb'; +import ApplicationDetails from 'components/applications/ApplicationDetails'; +import ApplicationGallery from 'components/applications/ApplicationGallery'; +import ApplicationsToolbar from 'components/applications/ApplicationsToolbar'; +import { apiURL } from 'utils/constants'; +import { applicationsDescription } from 'utils/constants'; +// clustersService is the Clusters gRPC service, which is used to get a list of resources. const clustersService = new ClustersPromiseClient(apiURL, null, null); -// Applications displays a list of applications (defined via the Application CRD). The applications can be filtered by -// cluster and namespace. -// When a application is selected it is shown in a drawer with some additional details, like resources, metrics, logs -// and traces. +export interface IScope { + clusters: 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). const Applications: React.FunctionComponent = () => { - const [applications, setApplications] = useState<Application[]>([]); - const [selectedApplication, setSelectedApplication] = useState<Application | undefined>(undefined); + const [scope, setScope] = useState<IScope | undefined>(undefined); + const [applications, setApplications] = useState<Application.AsObject[]>([]); + const [selectedApplication, setSelectedApplication] = useState<Application.AsObject | undefined>(undefined); const [error, setError] = useState<string>(''); - const [isLoading, setIsLoading] = useState<boolean>(false); - - // fetchApplications is the function to fetch all applications for a list of clusters and namespaces from the gRPC - // API. The function is used via the onFilter property of the Filter component. - const fetchApplications = async (clusters: string[], namespaces: string[]): Promise<void> => { - try { - if (clusters.length === 0 || namespaces.length === 0) { - throw new Error('You must select a cluster and a namespace'); - } else { - setIsLoading(true); + // 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 getApplicationsRequest = new GetApplicationsRequest(); - getApplicationsRequest.setClustersList(clusters); - getApplicationsRequest.setNamespacesList(namespaces); + getApplicationsRequest.setClustersList(scope.clusters); + getApplicationsRequest.setNamespacesList(scope.namespaces); const getApplicationsResponse: GetApplicationsResponse = await clustersService.getApplications( getApplicationsRequest, null, ); - const tmpApplications = getApplicationsResponse.getApplicationsList(); - - if (tmpApplications.length > 0) { - setError(''); - setApplications(tmpApplications); - } else { - setError('No applications were found, adjust the cluster and namespace filter.'); - } - - setIsLoading(false); + setApplications(getApplicationsResponse.toObject().applicationsList); + setError(''); + } catch (err) { + setError(err.message); } - } catch (err) { - setError(err.message); - setIsLoading(false); } - }; + }, [scope]); + + // useEffect is used to call the fetchApplications function every time the list of clusters and namespaces (scope), + // changes. + useEffect(() => { + fetchApplications(); + }, [fetchApplications]); return ( <React.Fragment> @@ -74,31 +69,39 @@ const Applications: React.FunctionComponent = () => { Applications

{applicationsDescription}

- +
setSelectedApplication(undefined)} /> + setSelectedApplication(undefined)} + /> ) : undefined } > - - {error ? ( - + + {!scope ? ( + +

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 ? ( +

{error}

) : ( - - {applications.map((application, index) => ( - - - - ))} - + )}
diff --git a/app/src/components/applications/ApplicationsToolbar.tsx b/app/src/components/applications/ApplicationsToolbar.tsx new file mode 100644 index 000000000..22e2873df --- /dev/null +++ b/app/src/components/applications/ApplicationsToolbar.tsx @@ -0,0 +1,100 @@ +import { + Button, + ButtonVariant, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + ToolbarToggleGroup, +} from '@patternfly/react-core'; +import React, { useContext, useState } from 'react'; +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; +} + +// 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, +}: IApplicationsToolbarProps) => { + const clustersContext = useContext(ClustersContext); + const [selectedClusters, setSelectedClusters] = useState([clustersContext.clusters[0]]); + const [selectedNamespaces, setSelectedNamespaces] = useState([]); + + // 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. + const selectCluster = (cluster: string): void => { + if (cluster === '') { + setSelectedClusters([]); + } else { + if (selectedClusters.includes(cluster)) { + setSelectedClusters(selectedClusters.filter((item) => item !== cluster)); + } else { + setSelectedClusters([...selectedClusters, cluster]); + } + } + }; + + // selectNamespace adds/removes the given namespace to the list of selected namespaces. When the namespace value is an + // empty string the selected namespaces list is cleared. + const selectNamespace = (namespace: string): void => { + if (namespace === '') { + setSelectedNamespaces([]); + } else { + if (selectedNamespaces.includes(namespace)) { + setSelectedNamespaces(selectedNamespaces.filter((item) => item !== namespace)); + } else { + setSelectedNamespaces([...selectedNamespaces, namespace]); + } + } + }; + + return ( + + + } breakpoint="lg"> + + + + + + + + + + + + + + + ); +}; + +export default ApplicationsToolbar; diff --git a/app/src/components/applications/details/DetailsLink.tsx b/app/src/components/applications/details/DetailsLink.tsx deleted file mode 100644 index eeac7fae0..000000000 --- a/app/src/components/applications/details/DetailsLink.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Link, useLocation } from 'react-router-dom'; -import React, { useEffect, useState } from 'react'; - -import { Application } from 'generated/proto/application_pb'; - -interface IDetailsLinkProps { - application: Application; -} - -// DetailsLink renders the link to the details page for an application inside the DrawerPanel of the applications page. -// Everytime when the location.search parameter (query parameters) are changing, we are adding the new parameters to the -// link, so that for example a change of the selected time range is also used in the details page. -const DetailsLink: React.FunctionComponent = ({ application }: IDetailsLinkProps) => { - const location = useLocation(); - - const [link, setLink] = useState( - `/applications/${application.getCluster()}/${application.getNamespace()}/${application.getName()}`, - ); - - useEffect(() => { - setLink( - `/applications/${application.getCluster()}/${application.getNamespace()}/${application.getName()}${ - location.search - }`, - ); - }, [application, location.search]); - - return Details; -}; - -export default DetailsLink; diff --git a/app/src/components/applications/details/DrawerPanel.tsx b/app/src/components/applications/details/DrawerPanel.tsx deleted file mode 100644 index 1981ed246..000000000 --- a/app/src/components/applications/details/DrawerPanel.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { - DrawerActions, - DrawerCloseButton, - DrawerHead, - DrawerPanelBody, - DrawerPanelContent, - List, - ListItem, - ListVariant, -} from '@patternfly/react-core'; -import React, { useRef, useState } from 'react'; -import { Link } from 'react-router-dom'; - -import Tabs, { DEFAULT_TAB } from 'components/applications/details/Tabs'; -import { Application } from 'generated/proto/application_pb'; -import DetailsLink from 'components/applications/details/DetailsLink'; -import TabsContent from 'components/applications/details/TabsContent'; -import Title from 'components/shared/Title'; - -interface IDrawerPanelProps { - application: Application; - close: () => void; -} - -// DrawerPanel is the drawer panel for an application. It is used to display application details in the applications -// page. The details contains information for resources, metrics, logs and traces. -const DrawerPanel: React.FunctionComponent = ({ application, close }: IDrawerPanelProps) => { - const [tab, setTab] = useState(DEFAULT_TAB); - const refResourcesContent = useRef(null); - const refMetricsContent = useRef(null); - const refLogsContent = useRef(null); - - return ( - - - - <DrawerActions className="kobs-drawer-actions"> - <DrawerCloseButton onClose={close} /> - </DrawerActions> - </DrawerHead> - - <DrawerPanelBody className="kobs-drawer-panel-body"> - <List variant={ListVariant.inline}> - <ListItem> - <DetailsLink application={application} /> - </ListItem> - {application.getLinksList().map((link, index) => ( - <ListItem key={index}> - <Link target="_blank" to={link.getLink}> - {link.getTitle()} - </Link> - </ListItem> - ))} - </List> - - <Tabs - tab={tab} - setTab={(t: string): void => setTab(t)} - refResourcesContent={refResourcesContent} - refMetricsContent={refMetricsContent} - refLogsContent={refLogsContent} - /> - - <TabsContent - application={application} - tab={tab} - refResourcesContent={refResourcesContent} - refMetricsContent={refMetricsContent} - refLogsContent={refLogsContent} - /> - </DrawerPanelBody> - </DrawerPanelContent> - ); -}; - -export default DrawerPanel; diff --git a/app/src/components/applications/details/NotDefined.tsx b/app/src/components/applications/details/NotDefined.tsx deleted file mode 100644 index 6e6506695..000000000 --- a/app/src/components/applications/details/NotDefined.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Button, EmptyState, EmptyStateBody, EmptyStateIcon, Title } from '@patternfly/react-core'; -import React from 'react'; - -interface INotDefinedProps { - title: string; - description: string; - documentation: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - icon: React.ComponentType<any>; -} - -// NotDefined is the component, which is displayed when a component of an application isn't defined. It contains a -// title, description, icon and a link to the corresponding documentation. -const NotDefined: React.FunctionComponent<INotDefinedProps> = ({ - title, - description, - icon, - documentation, -}: INotDefinedProps) => { - return ( - <EmptyState> - <EmptyStateIcon icon={icon} /> - <Title headingLevel="h4" size="lg"> - {title} - - {description} - - - ); -}; - -export default NotDefined; diff --git a/app/src/components/applications/details/Tabs.tsx b/app/src/components/applications/details/Tabs.tsx deleted file mode 100644 index 89f28830c..000000000 --- a/app/src/components/applications/details/Tabs.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Tabs as PatternflyTabs, Tab, TabTitleText } from '@patternfly/react-core'; -import React from 'react'; - -// DEFAULT_TAB is the first tab, which is selected in the application view. -export const DEFAULT_TAB = 'resources'; - -interface ITabsParams { - tab: string; - setTab(tab: string): void; - refResourcesContent: React.RefObject; - refMetricsContent: React.RefObject; - refLogsContent: React.RefObject; -} - -// Tabs renders the tabs header, which are used by the user to select a section he wants to view for an application. -// We can not use the tab state, within this component, because then the tab change isn't reflected in the TabsContent -// component. So that we have to manage the refs and tab in the parent component. -const Tabs: React.FunctionComponent = ({ - tab, - setTab, - refResourcesContent, - refMetricsContent, - refLogsContent, -}: ITabsParams) => { - return ( - setTab(tabIndex.toString())} - > - Resources} - tabContentId="refResources" - tabContentRef={refResourcesContent} - /> - Metrics} - tabContentId="refMetrics" - tabContentRef={refMetricsContent} - /> - Logs} - tabContentId="refLogs" - tabContentRef={refLogsContent} - /> - - ); -}; - -export default Tabs; diff --git a/app/src/components/applications/details/TabsContent.tsx b/app/src/components/applications/details/TabsContent.tsx deleted file mode 100644 index f767a9017..000000000 --- a/app/src/components/applications/details/TabsContent.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React, { useState } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; -import { TabContent } from '@patternfly/react-core'; - -import { Application } from 'generated/proto/application_pb'; -import { IDatasourceOptions } from 'utils/proto'; -import Logs from 'components/applications/details/logs/Logs'; -import Metrics from 'components/applications/details/metrics/Metrics'; -import Resources from 'components/applications/details/resources/Resources'; - -// datasourceOptionsFromLocationSearch is used to parse all query parameters during the first rendering of the -// TabsContent component. When the parameters are not set we return some default options for the datasources. Because it -// could happen that only some parameters are set via location.search, we have to check each property if it contains a -// valid value. If this is the case we are overwriting the default value. -const datasourceOptionsFromLocationSearch = (): IDatasourceOptions => { - const params = new URLSearchParams(window.location.search); - return { - resolution: params.get('resolution') ? (params.get('resolution') as string) : '', - timeEnd: params.get('timeEnd') ? parseInt(params.get('timeEnd') as string) : Math.floor(Date.now() / 1000), - timeStart: params.get('timeStart') - ? parseInt(params.get('timeStart') as string) - : Math.floor(Date.now() / 1000) - 3600, - }; -}; - -// createSearch creates a string, which can be used within the history.push function as search parameter. For that we -// are looping over each key of the IDatasourceOptions interface and if it contains a value, this value will be added to -// the parameters. -const createSearch = (options: IDatasourceOptions): string => { - const params: string[] = []; - - let option: keyof IDatasourceOptions; - for (option in options) { - if (options[option]) { - params.push(`${option}=${options[option]}`); - } - } - - return `?${params.join('&')}`; -}; - -interface ITabsContent { - application: Application; - tab: string; - refResourcesContent: React.RefObject; - refMetricsContent: React.RefObject; - refLogsContent: React.RefObject; -} - -// TabsContent renders the content for a selected tab from the Tabs component. We also manage the datasource options, -// within this component, so that we can share the selected time range between metrics, logs and traces. -// When the datasource options are changed, we also reflect this change in the URL via query parameters, so that a user -// can share his current view with other users. -const TabsContent: React.FunctionComponent = ({ - application, - tab, - refResourcesContent, - refMetricsContent, - refLogsContent, -}: ITabsContent) => { - const history = useHistory(); - const location = useLocation(); - const [datasourceOptions, setDatasourceOptions] = useState(datasourceOptionsFromLocationSearch()); - - const changeDatasourceOptions = (options: IDatasourceOptions): void => { - setDatasourceOptions(options); - - history.push({ - pathname: location.pathname, - search: createSearch(options), - }); - }; - - return ( - - -
- -
-
- - - {/* We have to check if the refMetricsContent is not null, because otherwise the Metrics component will be shown below the resources component. */} -
- {refMetricsContent.current ? ( - - ) : null} -
-
- - - {/* We have to check if the refLogsContent is not null, because otherwise the Logs component will be shown below the resources component. */} -
- {refLogsContent.current ? ( - - ) : null} -
-
-
- ); -}; - -export default TabsContent; diff --git a/app/src/components/applications/details/logs/Elasticsearch.tsx b/app/src/components/applications/details/logs/Elasticsearch.tsx deleted file mode 100644 index fc217f901..000000000 --- a/app/src/components/applications/details/logs/Elasticsearch.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { Alert, AlertVariant, Button } from '@patternfly/react-core'; -import React, { useCallback, useEffect, useState } from 'react'; - -import { DatasourceLogsBucket, GetLogsRequest, GetLogsResponse } from 'generated/proto/datasources_pb'; -import { ApplicationLogsQuery } from 'generated/proto/application_pb'; -import Buckets from 'components/datasources/elasticsearch/Buckets'; -import { DatasourcesPromiseClient } from 'generated/proto/datasources_grpc_web_pb'; -import Documents from 'components/datasources/elasticsearch/Documents'; -import { IDatasourceOptions } from 'utils/proto'; -import { IDocument } from 'components/datasources/elasticsearch/helpers'; -import { apiURL } from 'utils/constants'; -import { convertDatasourceOptionsToProto } from 'utils/proto'; - -const datasourcesService = new DatasourcesPromiseClient(apiURL, null, null); - -export interface IElasticsearchProps { - query?: string; - fields?: string[]; - datasourceName: string; - datasourceType: string; - datasourceOptions: IDatasourceOptions; -} - -// Elasticsearhc implements the Elasticsearch UI for kobs. It can be used to query a configured Elasticsearch instance -// and show the logs in a table. -const Elasticsearch: React.FunctionComponent = ({ - query, - fields, - datasourceName, - datasourceType, - datasourceOptions, -}: IElasticsearchProps) => { - const [hits, setHits] = useState(0); - const [took, setTook] = useState(0); - const [documents, setDocuments] = useState([]); - const [buckets, setBuckets] = useState([]); - const [error, setError] = useState(''); - - // fetchLogs fetches the logs for a given query. For the applications view, we do not care about infinite scrolling. - // When a user wants to see more then the fetched logs, he has to go to the datasource view. - const fetchLogs = useCallback(async (): Promise => { - try { - if (query) { - const logsQuery = new ApplicationLogsQuery(); - logsQuery.setQuery(query); - - const getLogsRequest = new GetLogsRequest(); - getLogsRequest.setName(datasourceName); - getLogsRequest.setScrollid(''); - getLogsRequest.setOptions(convertDatasourceOptionsToProto(datasourceOptions)); - getLogsRequest.setQuery(logsQuery); - - const getLogsResponse: GetLogsResponse = await datasourcesService.getLogs(getLogsRequest, null); - - const parsed = JSON.parse(getLogsResponse.getLogs()); - if (parsed.length === 0) { - throw new Error('No documents were found'); - } else { - if (getLogsResponse.toObject().bucketsList.length > 0) { - setBuckets(getLogsResponse.toObject().bucketsList); - } - - setDocuments(parsed); - setHits(getLogsResponse.getHits()); - setTook(getLogsResponse.getTook()); - setError(''); - } - } - } catch (err) { - setError(err.message); - } - }, [query, datasourceName, datasourceOptions]); - - useEffect(() => { - fetchLogs(); - }, [fetchLogs]); - - return ( - - {error ? ( - -

 

- -

{error}

-
-
- ) : ( - -

 

- - {buckets.length > 0 ? : null} - -

 

- - {documents.length > 0 ? ( - - ) : null} - -

 

- - -
- )} -
- ); -}; - -export default Elasticsearch; diff --git a/app/src/components/applications/details/logs/Logs.tsx b/app/src/components/applications/details/logs/Logs.tsx deleted file mode 100644 index 36c397d63..000000000 --- a/app/src/components/applications/details/logs/Logs.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { Alert, AlertActionLink, AlertVariant } from '@patternfly/react-core'; -import React, { useCallback, useEffect, useState } from 'react'; -import ChartAreaIcon from '@patternfly/react-icons/dist/js/icons/chart-area-icon'; - -import { Application, ApplicationLogsQuery } from 'generated/proto/application_pb'; -import { GetDatasourceRequest, GetDatasourceResponse } from 'generated/proto/datasources_pb'; -import { DatasourcesPromiseClient } from 'generated/proto/datasources_grpc_web_pb'; -import Elasticsearch from 'components/applications/details/logs/Elasticsearch'; -import { IDatasourceOptions } from 'utils/proto'; -import NotDefined from 'components/applications/details/NotDefined'; -import Toolbar from 'components/applications/details/logs/Toolbar'; -import { apiURL } from 'utils/constants'; - -const datasourcesService = new DatasourcesPromiseClient(apiURL, null, null); - -interface ILogsProps { - datasourceOptions: IDatasourceOptions; - setDatasourceOptions: (options: IDatasourceOptions) => void; - application: Application; -} - -// Logs is the component, which is shown inside the logs tab of an application. It is used as wrapper component for the -// toolbar and results component. For the results we show different components, depending on the datasource type. -const Logs: React.FunctionComponent = ({ - datasourceOptions, - setDatasourceOptions, - application, -}: ILogsProps) => { - const logs = application.getLogs(); - - const [datasourceName, setDatasourceName] = useState(''); - const [datasourceType, setDatasourceType] = useState(''); - const [query, setQuery] = useState(logs ? logs.getQueriesList()[0] : undefined); - const [error, setError] = useState(''); - - // fetchDatasourceDetails fetch all details, which are specific for a datasource. Currently this is only the type of - // the datasource, but can be extended in the future if needed. More information can be found in the datasources.proto - // file and the documentation for the GetDatasourceResponse message format. - const fetchDatasourceDetails = useCallback(async () => { - try { - if (!logs) { - throw new Error('Logs are not defined.'); - } else { - const getDatasourceRequest = new GetDatasourceRequest(); - getDatasourceRequest.setName(logs.getDatasource()); - - const getDatasourceResponse: GetDatasourceResponse = await datasourcesService.getDatasource( - getDatasourceRequest, - null, - ); - - const datasource = getDatasourceResponse.getDatasource(); - if (datasource) { - setDatasourceName(datasource.getName()); - setDatasourceType(datasource.getType()); - setError(''); - } else { - throw new Error('Datasource is not defined.'); - } - } - } catch (err) { - setError(err.message); - } - }, [logs]); - - useEffect(() => { - fetchDatasourceDetails(); - }, [fetchDatasourceDetails]); - - // If the logs seticon in the Application CR isn't defined, we return the NotDefined component, with a link to the - // documentation, where a user can find more information on who to define logs. - if (!logs) { - return ( - - ); - } - - // If an error occured during, we show the user the error, with an option to retry the request. - if (error) { - return ( - - Retry -
- } - > -

{error}

- - ); - } - - return ( -
- setQuery(q)} - /> - - {datasourceType === 'elasticsearch' ? ( - - ) : null} -
- ); -}; - -export default Logs; diff --git a/app/src/components/applications/details/logs/Toolbar.tsx b/app/src/components/applications/details/logs/Toolbar.tsx deleted file mode 100644 index fc482345a..000000000 --- a/app/src/components/applications/details/logs/Toolbar.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { - Dropdown, - DropdownItem, - DropdownToggle, - Toolbar as PatternflyToolbar, - ToolbarContent, - ToolbarGroup, - ToolbarItem, - ToolbarToggleGroup, -} from '@patternfly/react-core'; -import React, { useState } from 'react'; -import CaretDownIcon from '@patternfly/react-icons/dist/js/icons/caret-down-icon'; -import FilterIcon from '@patternfly/react-icons/dist/js/icons/filter-icon'; - -import { ApplicationLogsQuery } from 'generated/proto/application_pb'; -import { IDatasourceOptions } from 'utils/proto'; -import Options from 'components/applications/details/metrics/Options'; - -interface IToolbarProps { - datasourcenName: string; - datasourceType: string; - datasourceOptions: IDatasourceOptions; - setDatasourceOptions: (options: IDatasourceOptions) => void; - queries: ApplicationLogsQuery[]; - query?: ApplicationLogsQuery; - selectQuery: (q: ApplicationLogsQuery) => void; -} - -// Toolbar shows the options for a logs datasource, where the user can select the time range for which he wants to fetch -// the logs. The user can also select a defined query for an application. -const Toolbar: React.FunctionComponent = ({ - datasourcenName, - datasourceType, - datasourceOptions, - setDatasourceOptions, - queries, - query, - selectQuery, -}: IToolbarProps) => { - const [show, setShow] = useState(false); - - // selectDropdownItem is the function is called, when a user selects a query from the dropdown component. When a query - // is selected, we set this query as the current query and we close the dropdown. - const selectDropdownItem = (q: ApplicationLogsQuery): void => { - selectQuery(q); - setShow(false); - }; - - return ( - - - } breakpoint="lg"> - - - setShow(!show)} - toggleIndicator={CaretDownIcon} - > - {query ? query.getName() : 'Queries'} - - } - isOpen={show} - dropdownItems={queries.map((q, index) => ( - selectDropdownItem(q)} - description={q.getQuery()} - > - {q.getName()} - - ))} - /> - - - - - - - - - - - ); -}; - -export default Toolbar; diff --git a/app/src/components/applications/details/metrics/Chart.tsx b/app/src/components/applications/details/metrics/Chart.tsx deleted file mode 100644 index 27531ad61..000000000 --- a/app/src/components/applications/details/metrics/Chart.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { - Alert, - AlertActionLink, - AlertVariant, - Card, - CardActions, - CardBody, - CardHeader, - CardHeaderMain, -} from '@patternfly/react-core'; -import React, { useCallback, useEffect, useState } from 'react'; - -import { DatasourceMetrics, GetMetricsRequest, GetMetricsResponse } from 'generated/proto/datasources_pb'; -import { - IApplicationMetricsVariable, - IDatasourceOptions, - convertApplicationMetricsVariablesToProto, - convertDatasourceOptionsToProto, -} from 'utils/proto'; -import Actions from 'components/applications/details/metrics/charts/Actions'; -import { ApplicationMetricsChart } from 'generated/proto/application_pb'; -import { DatasourcesPromiseClient } from 'generated/proto/datasources_grpc_web_pb'; -import DefaultChart from 'components/applications/details/metrics/charts/Default'; -import EmptyStateSpinner from 'components/shared/EmptyStateSpinner'; -import SparklineChart from 'components/applications/details/metrics/charts/Sparkline'; -import { apiURL } from 'utils/constants'; - -const datasourcesService = new DatasourcesPromiseClient(apiURL, null, null); - -interface IChartProps { - datasourceName: string; - datasourceType: string; - datasourceOptions: IDatasourceOptions; - variables: IApplicationMetricsVariable[]; - chart: ApplicationMetricsChart; -} - -// Chart component is used to fetch the data for an chart and to render the chart within a Card component. -const Chart: React.FunctionComponent = ({ - datasourceName, - datasourceType, - datasourceOptions, - variables, - chart, -}: IChartProps) => { - const [data, setData] = useState([]); - const [interpolatedQueries, setInterpolatedQueries] = useState([]); - const [error, setError] = useState(''); - - // fetchData fetchs the data for a chart. If the gRPC call returns an error, we catch the error and set the - // corresponding error state. - const fetchData = useCallback(async () => { - try { - if (datasourceName !== '' && chart.getQueriesList().length > 0) { - const getMetricsRequest = new GetMetricsRequest(); - getMetricsRequest.setName(datasourceName); - getMetricsRequest.setOptions(convertDatasourceOptionsToProto(datasourceOptions)); - getMetricsRequest.setVariablesList(convertApplicationMetricsVariablesToProto(variables)); - getMetricsRequest.setQueriesList(chart.getQueriesList()); - - const getMetricsResponse: GetMetricsResponse = await datasourcesService.getMetrics(getMetricsRequest, null); - - setInterpolatedQueries(getMetricsResponse.getInterpolatedqueriesList()); - setData(getMetricsResponse.getMetricsList()); - setError(''); - } - } catch (err) { - setError(err.message); - } - }, [datasourceName, datasourceOptions, variables, chart]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - // If an error occured during the gRPC call we show the error message in the card body. - if (error) { - return ( - - - {chart.getTitle()} - - - - Retry - - } - > -

{error}

-
-
-
- ); - } - - // If the data length is zero, we show the empty state component with a spinner, because this only can happen on the - // first render. When the fetchData all was executed the data must be set or the error will be rendered befor this. - if (data.length === 0) { - return ( - - - {chart.getTitle()} - - - - - - ); - } - - return ( - - - {chart.getTitle()} - - - - - - {chart.getType() === 'sparkline' ? ( - - ) : ( - - )} - - - ); -}; - -export default Chart; diff --git a/app/src/components/applications/details/metrics/Charts.tsx b/app/src/components/applications/details/metrics/Charts.tsx deleted file mode 100644 index 3b020e2a4..000000000 --- a/app/src/components/applications/details/metrics/Charts.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Grid, GridItem, Title, gridSpans } from '@patternfly/react-core'; -import React, { useEffect, useRef, useState } from 'react'; - -import { IApplicationMetricsVariable, IDatasourceOptions } from 'utils/proto'; -import { ApplicationMetricsChart } from 'generated/proto/application_pb'; -import Chart from 'components/applications/details/metrics/Chart'; - -interface IChartsProps { - datasourceName: string; - datasourceType: string; - datasourceOptions: IDatasourceOptions; - variables: IApplicationMetricsVariable[]; - charts: ApplicationMetricsChart[]; -} - -// Charts renders a Grid of the user defined charts for an applicatication. The grid contains a small padding to the -// toolbar, which is rendered above. When the width of the grid is larger then 1200px, we apply the user defined size -// for each chart. If the width is below this value each chart will be rendered accross the complete width of the grid. -const Charts: React.FunctionComponent = ({ - datasourceName, - datasourceType, - datasourceOptions, - variables, - charts, -}: IChartsProps) => { - const refGrid = useRef(null); - const [width, setWidth] = useState(0); - - // useEffect is executed on every render, to determin the size of the grid and apply the user defined size for charts - // if necessary. - useEffect(() => { - if (refGrid && refGrid.current) { - setWidth(refGrid.current.getBoundingClientRect().width); - } - }, []); - - return ( -
- - {charts.map((chart, index) => ( - = 1200 && chart.getSize() > 0 && chart.getSize() <= 12 && chart.getType() !== 'divider' - ? (chart.getSize() as gridSpans) - : 12 - } - > - {chart.getType() === 'divider' ? ( - - {chart.getTitle()} - - ) : ( - - )} - - ))} - -
- ); -}; - -export default Charts; diff --git a/app/src/components/applications/details/metrics/Metrics.tsx b/app/src/components/applications/details/metrics/Metrics.tsx deleted file mode 100644 index 5e463786e..000000000 --- a/app/src/components/applications/details/metrics/Metrics.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { Alert, AlertActionLink, AlertVariant } from '@patternfly/react-core'; -import React, { useCallback, useEffect, useState } from 'react'; -import ChartAreaIcon from '@patternfly/react-icons/dist/js/icons/chart-area-icon'; - -import { GetDatasourceRequest, GetDatasourceResponse } from 'generated/proto/datasources_pb'; -import { - IApplicationMetricsVariable, - IDatasourceOptions, - convertApplicationMetricsVariablesFromProto, -} from 'utils/proto'; -import { Application } from 'generated/proto/application_pb'; -import Charts from 'components/applications/details/metrics/Charts'; -import { DatasourcesPromiseClient } from 'generated/proto/datasources_grpc_web_pb'; -import NotDefined from 'components/applications/details/NotDefined'; -import Toolbar from 'components/applications/details/metrics/Toolbar'; -import { apiURL } from 'utils/constants'; - -const datasourcesService = new DatasourcesPromiseClient(apiURL, null, null); - -interface IMetricsProps { - datasourceOptions: IDatasourceOptions; - setDatasourceOptions: (options: IDatasourceOptions) => void; - application: Application; -} - -// Metrics the metrics component is used to display the metrics for an application. The metrics view consist of a -// toolbar, to display variables and different datasource specific options for the queries. It also contains a charts -// view, to display all user defined charts. -const Metrics: React.FunctionComponent = ({ - datasourceOptions, - setDatasourceOptions, - application, -}: IMetricsProps) => { - const metrics = application.getMetrics(); - - const [datasourceName, setDatasourceName] = useState(''); - const [datasourceType, setDatasourceType] = useState(''); - const [variables, setVariables] = useState( - metrics ? convertApplicationMetricsVariablesFromProto(metrics.getVariablesList()) : [], - ); - const [error, setError] = useState(''); - - // fetchDatasourceDetails fetch all details, which are specific for a datasource. Currently this is only the type of - // the datasource, but can be extended in the future if needed. More information can be found in the datasources.proto - // file and the documentation for the GetDatasourceResponse message format. - const fetchDatasourceDetails = useCallback(async () => { - try { - if (!metrics) { - throw new Error('Metrics are not defined.'); - } else { - const getDatasourceRequest = new GetDatasourceRequest(); - getDatasourceRequest.setName(metrics.getDatasource()); - - const getDatasourceResponse: GetDatasourceResponse = await datasourcesService.getDatasource( - getDatasourceRequest, - null, - ); - - const datasource = getDatasourceResponse.getDatasource(); - if (datasource) { - setDatasourceName(datasource.getName()); - setDatasourceType(datasource.getType()); - setError(''); - } else { - throw new Error('Datasource is not defined.'); - } - } - } catch (err) { - setError(err.message); - } - }, [metrics]); - - useEffect(() => { - fetchDatasourceDetails(); - }, [fetchDatasourceDetails]); - - // If the metrics seticon in the Application CR isn't defined, we return the NotDefined component, with a link to the - // documentation, where a user can find more information on who to define metrics. - if (!metrics) { - return ( - - ); - } - - // If an error occured during, we show the user the error, with an option to retry the request. - if (error) { - return ( - - Retry - - } - > -

{error}

-
- ); - } - - return ( - - setVariables(vars)} - /> - - - - ); -}; - -export default Metrics; diff --git a/app/src/components/applications/details/metrics/Options.tsx b/app/src/components/applications/details/metrics/Options.tsx deleted file mode 100644 index ea9c0b4a2..000000000 --- a/app/src/components/applications/details/metrics/Options.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { - Button, - ButtonVariant, - Form, - FormGroup, - Level, - LevelItem, - Modal, - ModalVariant, - SimpleList, - SimpleListItem, - TextInput, -} from '@patternfly/react-core'; -import React, { useEffect, useState } from 'react'; - -import { IDatasourceOptions } from 'utils/proto'; -import { formatTime } from 'utils/helpers'; - -interface IOptionsProps { - type: string; - options: IDatasourceOptions; - setOptions: (options: IDatasourceOptions) => void; -} - -// Options is the component, where the user can select various options for the current view. The user can select a time -// range for all queries via the quick select option or he can specify a start and end time via the input fields. Later -// we can also display datasource specific options within the modal component. -const Options: React.FunctionComponent = ({ type, options, setOptions }: IOptionsProps) => { - const [show, setShow] = useState(false); - const [timeStart, setTimeStart] = useState(formatTime(options.timeStart)); - const [timeEnd, setTimeEnd] = useState(formatTime(options.timeEnd)); - const [timeStartError, setTimeStartError] = useState(''); - const [timeEndError, setTimeEndError] = useState(''); - - const [resolution, setResolution] = useState(options.resolution ? options.resolution : ''); - - // apply parses the value of the start and end input fields. If the user provided a correct data/time format, we - // change the start and end time to the new values. If the string couldn't be parsed, the user will see an error below - // the corresponding input field. - const apply = (): void => { - // Get a new date object for the users current timezone. This allows us to ignore the timezone, while parsing the - // provided time string. The parsed date object will be in UTC, to transform the parsed date into the users timezone - // we have to add the minutes between UTC and the users timezon (getTimezoneOffset()). - const d = new Date(); - - const parsedTimeStart = new Date(timeStart.replace(' ', 'T') + 'Z'); - const parsedTimeEnd = new Date(timeEnd.replace(' ', 'T') + 'Z'); - - parsedTimeStart.setMinutes(parsedTimeStart.getMinutes() + d.getTimezoneOffset()); - parsedTimeEnd.setMinutes(parsedTimeEnd.getMinutes() + d.getTimezoneOffset()); - - if (parsedTimeStart.toString() === 'Invalid Date') { - setTimeStartError('Invalid time format.'); - setTimeEndError(''); - } else if (parsedTimeEnd.toString() === 'Invalid Date') { - setTimeStartError(''); - setTimeEndError('Invalid time format.'); - } else { - setTimeStartError(''); - setTimeEndError(''); - setOptions({ - ...options, - resolution: resolution, - timeEnd: Math.floor(parsedTimeEnd.getTime() / 1000), - timeStart: Math.floor(parsedTimeStart.getTime() / 1000), - }); - setShow(false); - } - }; - - // quick is the function for the quick select option. We always use the current time in seconds and substract the - // seconds specified in the quick select option. - const quick = (seconds: number): void => { - setOptions({ - ...options, - resolution: resolution, - timeEnd: Math.floor(Date.now() / 1000), - timeStart: Math.floor(Date.now() / 1000) - seconds, - }); - setShow(false); - }; - - // useEffect is used to update the UI, every time a dasource options changes. - useEffect(() => { - setTimeStart(formatTime(options.timeStart)); - setTimeEnd(formatTime(options.timeEnd)); - - setResolution(options.resolution ? options.resolution : ''); - }, [options.timeEnd, options.timeStart, options.resolution]); - - return ( - - - setShow(false)} - actions={[ - , - , - ]} - > - - -
- - setTimeStart(value)} - /> - - - setTimeEnd(value)} - /> - -
-
- - - quick(300)}>Last 5 Minutes - quick(900)}>Last 15 Minutes - quick(1800)}>Last 30 Minutes - quick(3600)}>Last 1 Hour - quick(10800)}>Last 3 Hours - quick(21600)}>Last 6 Hours - quick(43200)}>Last 12 Hours - - - - - quick(86400)}>Last 1 Day - quick(172800)}>Last 2 Days - quick(604800)}>Last 7 Days - quick(2592000)}>Last 30 Days - quick(7776000)}>Last 90 Days - quick(15552000)}>Last 6 Months - quick(31536000)}>Last 1 Year - - - {type === 'prometheus' ? ( - -
- - setResolution(value)} - /> - -
-
- ) : null} -
-
-
- ); -}; - -export default Options; diff --git a/app/src/components/applications/details/metrics/Toolbar.tsx b/app/src/components/applications/details/metrics/Toolbar.tsx deleted file mode 100644 index 0cc726040..000000000 --- a/app/src/components/applications/details/metrics/Toolbar.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { - Alert, - AlertActionLink, - AlertVariant, - Toolbar as PatternflyToolbar, - ToolbarContent, - ToolbarGroup, - ToolbarItem, - ToolbarToggleGroup, -} from '@patternfly/react-core'; -import React, { useCallback, useEffect, useState } from 'react'; -import FilterIcon from '@patternfly/react-icons/dist/js/icons/filter-icon'; - -import { GetVariablesRequest, GetVariablesResponse } from 'generated/proto/datasources_pb'; -import { - IApplicationMetricsVariable, - IDatasourceOptions, - convertApplicationMetricsVariablesFromProto, - convertApplicationMetricsVariablesToProto, - convertDatasourceOptionsToProto, -} from 'utils/proto'; -import { DatasourcesPromiseClient } from 'generated/proto/datasources_grpc_web_pb'; -import Options from 'components/applications/details/metrics/Options'; -import Variable from 'components/applications/details/metrics/Variable'; -import { apiURL } from 'utils/constants'; - -const datasourcesService = new DatasourcesPromiseClient(apiURL, null, null); - -interface IToolbarProps { - datasourcenName: string; - datasourceType: string; - datasourceOptions: IDatasourceOptions; - setDatasourceOptions: (options: IDatasourceOptions) => void; - variables: IApplicationMetricsVariable[]; - setVariables: (variables: IApplicationMetricsVariable[]) => void; -} - -// Toolbar component displays a list of all variables and an options field. The variables are displayed via a dropdown -// and can be selected by the user. If the user selects a new value, the variables property will be changed via the -// setVariables function, so that the change is also propergated to the corresponding charts. The same counts for the -// datasource options. -const Toolbar: React.FunctionComponent = ({ - datasourcenName, - datasourceType, - datasourceOptions, - setDatasourceOptions, - variables, - setVariables, -}: IToolbarProps) => { - const [error, setError] = useState(''); - - // onSelectVariableValue is executed, when the user selects a new value for a variable. It will create a copy of the - // current variables, changes the value and sets the new values in the parent component. - const onSelectVariableValue = (value: string, index: number): void => { - const tmpVariables = [...variables]; - tmpVariables[index].value = value; - setVariables(tmpVariables); - }; - - // fetchVariables is used to fetch all values for all variables. When we successfully fetched all values we change, - // the passed in variables property. - // HACK: Since this ends in an endless rerendering and fetching we have omit the setVariables in the dependency array. - // We also have to compare the JSON representation of the variables prop and the loaded variables, to omit unnecessary - // rerenderings. Maybe we can also do this before the fetch, so that we do not fetch the variables twice. - const fetchVariables = useCallback(async () => { - try { - if (datasourcenName !== '' && variables.length > 0) { - const getVariablesRequest = new GetVariablesRequest(); - getVariablesRequest.setName(datasourcenName); - getVariablesRequest.setOptions(convertDatasourceOptionsToProto(datasourceOptions)); - getVariablesRequest.setVariablesList(convertApplicationMetricsVariablesToProto(variables)); - - const getVariablesResponse: GetVariablesResponse = await datasourcesService.getVariables( - getVariablesRequest, - null, - ); - - const tmpVariables = convertApplicationMetricsVariablesFromProto(getVariablesResponse.getVariablesList()); - if (JSON.stringify(tmpVariables) !== JSON.stringify(variables)) { - setVariables(tmpVariables); - } - setError(''); - } - } catch (err) { - setError(err.message); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [datasourcenName, datasourceOptions, variables]); - - useEffect(() => { - fetchVariables(); - }, [fetchVariables]); - - // If an error occured during, we show the user the error, with an option to retry the request. - if (error) { - return ( - - Retry - - } - > -

{error}

-
- ); - } - - return ( - - - } breakpoint="lg"> - - {variables.map((variable, index) => ( - - onSelectVariableValue(value, index)} - /> - - ))} - - - - - - - - - - ); -}; - -export default Toolbar; diff --git a/app/src/components/applications/details/metrics/Variable.tsx b/app/src/components/applications/details/metrics/Variable.tsx deleted file mode 100644 index def897867..000000000 --- a/app/src/components/applications/details/metrics/Variable.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { useState } from 'react'; -import { Select, SelectOption, SelectOptionObject, SelectVariant } from '@patternfly/react-core'; - -import { IApplicationMetricsVariable } from 'utils/proto'; - -interface IVariableProps { - variable: IApplicationMetricsVariable; - selectValue: (value: string) => void; -} - -// Variable is the component tp render a single variable in a Select component. When the user selects a new value, via -// use the passed in selectValue function to change the variable. -const Variable: React.FunctionComponent = ({ variable, selectValue }: IVariableProps) => { - const [show, setShow] = useState(false); - - const onSelect = ( - event: React.MouseEvent | React.ChangeEvent, - value: string | SelectOptionObject, - ): void => { - selectValue(value as string); - setShow(false); - }; - - return ( - - ); -}; - -export default Variable; diff --git a/app/src/components/applications/details/metrics/charts/Actions.tsx b/app/src/components/applications/details/metrics/charts/Actions.tsx deleted file mode 100644 index e8747f048..000000000 --- a/app/src/components/applications/details/metrics/charts/Actions.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Dropdown, DropdownItem, KebabToggle } from '@patternfly/react-core'; -import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; - -import { IDatasourceOptions } from 'utils/proto'; - -interface IActionsProps { - datasourceName: string; - datasourceType: string; - datasourceOptions: IDatasourceOptions; - interpolatedQueries: string[]; -} - -// Actions is a dropdown component, which provides various actions for a chart. For example it can be used to display a -// link for each query in the chart. When the user click on the link, he will be redirected to the corresponding -// datasource page, with the query and time data prefilled. -const Actions: React.FunctionComponent = ({ - datasourceName, - datasourceType, - datasourceOptions, - interpolatedQueries, -}: IActionsProps) => { - const [show, setShow] = useState(false); - - return ( - setShow(!show)} />} - isOpen={show} - isPlain={true} - position="right" - dropdownItems={interpolatedQueries.map((query, index) => ( - - Explore {query} - - } - /> - ))} - /> - ); -}; - -export default Actions; diff --git a/app/src/components/applications/details/metrics/charts/Default.tsx b/app/src/components/applications/details/metrics/charts/Default.tsx deleted file mode 100644 index 9d13eb9c9..000000000 --- a/app/src/components/applications/details/metrics/charts/Default.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { - Chart, - ChartArea, - ChartAxis, - ChartBar, - ChartGroup, - ChartLegendTooltip, - ChartLine, - ChartStack, - ChartThemeColor, - createContainer, -} from '@patternfly/react-charts'; -import React, { useEffect, useRef, useState } from 'react'; - -import { DatasourceMetrics } from 'generated/proto/datasources_pb'; -import { IDatasourceMetricsData } from 'utils/proto'; -import { formatTime } from 'utils/helpers'; - -interface ILabels { - datum: IDatasourceMetricsData; -} - -export interface IDefaultProps { - type: string; - unit: string; - stacked: boolean; - disableLegend?: boolean; - metrics: DatasourceMetrics[]; -} - -// Default represents our default chart types: area, bar and line chart. We display the user defined unit at the y axis -// of the chart. If the user enabled the stacked option the chart is wrapped in a ChartStack instead of the ChartGroup -// component. -// The documentation for the different chart types can be found in the Patternfly documentation: -// - Area Chart: https://www.patternfly.org/v4/charts/area-chart -// - Bar Chart: https://www.patternfly.org/v4/charts/bar-chart -// - Line Chart: https://www.patternfly.org/v4/charts/line-chart -// -// NOTE: Currently it is not possible to select a single time series in the chart. This should be changed in the future, -// by using an interactive legend: https://www.patternfly.org/v4/charts/legend-chart#interactive-legend -const Default: React.FunctionComponent = ({ - type, - unit, - stacked, - disableLegend, - metrics, -}: IDefaultProps) => { - const refChart = useRef(null); - const [width, setWidth] = useState(0); - const [height, setHeight] = useState(0); - - // useEffect is executed on every render of this component. This is needed, so that we are able to use a width of 100% - // and a static height for the chart. - useEffect(() => { - if (refChart && refChart.current) { - setWidth(refChart.current.getBoundingClientRect().width); - setHeight(refChart.current.getBoundingClientRect().height); - } - }, []); - - const CursorVoronoiContainer = createContainer('voronoi', 'cursor'); - const legendData = metrics.map((metric, index) => ({ childName: `index${index}`, name: metric.getLabel() })); - const series = metrics.map((metric, index) => - type === 'area' ? ( - - ) : type === 'bar' ? ( - - ) : ( - - ), - ); - - return ( -
- `${datum.y} ${unit}`} - labelComponent={ - formatTime(Math.floor(point.x / 1000))} - /> - } - mouseFollowTooltips - voronoiDimension="x" - voronoiPadding={0} - /> - } - height={height} - legendData={legendData} - legendPosition={disableLegend ? undefined : 'bottom'} - padding={{ bottom: disableLegend ? 0 : 60, left: 60, right: 0, top: 0 }} - scale={{ x: 'time', y: 'linear' }} - themeColor={ChartThemeColor.multiOrdered} - width={width} - > - - - {stacked ? {series} : {series}} - -
- ); -}; - -export default Default; diff --git a/app/src/components/applications/details/metrics/charts/Sparkline.tsx b/app/src/components/applications/details/metrics/charts/Sparkline.tsx deleted file mode 100644 index 8fa1265dd..000000000 --- a/app/src/components/applications/details/metrics/charts/Sparkline.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ChartArea, ChartGroup } from '@patternfly/react-charts'; -import React, { useEffect, useRef, useState } from 'react'; - -import { DatasourceMetrics } from 'generated/proto/datasources_pb'; - -export interface ISparklineProps { - unit: string; - metrics: DatasourceMetrics[]; -} - -// Sparkline displays a sparkline chart. The complete documentation for sparklines can be found in the Patternfly -// documentation https://www.patternfly.org/v4/charts/sparkline-chart. We also display the last/current value in the -// center of the sparkline, including the user defined unit. -const Sparkline: React.FunctionComponent = ({ unit, metrics }: ISparklineProps) => { - const refChart = useRef(null); - const [width, setWidth] = useState(0); - const [height, setHeight] = useState(0); - - // useEffect is executed on every render of this component. This is needed, so that we are able to use a width of 100% - // and a static height for the chart. - useEffect(() => { - if (refChart && refChart.current) { - setWidth(refChart.current.getBoundingClientRect().width); - setHeight(refChart.current.getBoundingClientRect().height); - } - }, []); - - return ( -
-
- {metrics[0].getDataList()[metrics[0].getDataList().length - 1].getY()} {unit} -
- - {metrics.map((metric, index) => ( - - ))} - -
- ); -}; - -export default Sparkline; diff --git a/app/src/components/applications/details/resources/Resource.tsx b/app/src/components/applications/details/resources/Resource.tsx deleted file mode 100644 index 3b609833a..000000000 --- a/app/src/components/applications/details/resources/Resource.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { IRow, Table, TableBody, TableHeader } from '@patternfly/react-table'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import { GetResourcesRequest, GetResourcesResponse } from 'generated/proto/clusters_pb'; -import { emptyState, resources } from 'components/resources/shared/helpers'; -import { ClustersPromiseClient } from 'generated/proto/clusters_grpc_web_pb'; -import { apiURL } from 'utils/constants'; - -const clustersService = new ClustersPromiseClient(apiURL, null, null); - -interface IResourceProps { - cluster: string; - namespace: string; - kind: string; - selector: string; -} - -// Resource loads a list of resources for an application and shows them in a table. If the list of loaded recources is -// zero, we will display an empty state. -const Resource: React.FunctionComponent = ({ cluster, namespace, kind, selector }: IResourceProps) => { - const columns = useMemo( - () => (resources.hasOwnProperty(kind) ? resources[kind].columns : ['Name', 'Namespace', 'Cluster']), - [kind], - ); - const [rows, setRows] = useState(emptyState(columns.length, '')); - - // fetchResources is used to get all resources from the gRPC API for an specific application. For that we have to set - // the cluster, namespace and name of the application. Besides that the CR also defines the kind which should be - // loaded and the labelSelector. - const fetchResources = useCallback(async () => { - try { - const getResourcesRequest = new GetResourcesRequest(); - getResourcesRequest.setClustersList([cluster]); - getResourcesRequest.setNamespacesList([namespace]); - getResourcesRequest.setPath(resources[kind].path); - getResourcesRequest.setResource(resources[kind].resource); - getResourcesRequest.setParamname('labelSelector'); - getResourcesRequest.setParam(selector); - - const getResourcesResponse: GetResourcesResponse = await clustersService.getResources(getResourcesRequest, null); - const tmpRows = resources[kind].rows(getResourcesResponse.getResourcesList()); - - if (tmpRows.length > 0) { - setRows(tmpRows); - } else { - setRows(emptyState(columns.length, '')); - } - } catch (err) { - setRows(emptyState(columns.length, err.message)); - } - }, [cluster, namespace, kind, selector, columns]); - - useEffect(() => { - fetchResources(); - }, [fetchResources]); - - return ( - - - -
- ); -}; - -export default Resource; diff --git a/app/src/components/applications/details/resources/Resources.tsx b/app/src/components/applications/details/resources/Resources.tsx deleted file mode 100644 index 7e316dfdf..000000000 --- a/app/src/components/applications/details/resources/Resources.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Accordion, AccordionContent, AccordionItem, AccordionToggle } from '@patternfly/react-core'; -import React, { useState } from 'react'; -import CubesIcon from '@patternfly/react-icons/dist/js/icons/cubes-icon'; - -import { Application } from 'generated/proto/application_pb'; -import NotDefined from 'components/applications/details/NotDefined'; -import Resource from 'components/applications/details/resources/Resource'; -import { resources } from 'components/resources/shared/helpers'; - -interface IResourcesProps { - application: Application; -} - -// Resources is the component to show all resources, which are associated to the application. The resource are grouped -// by their kind in an accordion view. -const Resources: React.FunctionComponent = ({ application }: IResourcesProps) => { - const [expanded, setExpanded] = useState([]); - - // toogle is used to show / hide a selected kind. When the kind is already present in the expanded state array it will - // be removed. If not it will be added. - const toggle = (id: string): void => { - if (expanded.includes(id)) { - setExpanded(expanded.filter((item) => item !== id)); - } else { - setExpanded([...expanded, id]); - } - }; - - // If the length of the resource list is zero, we will show the NotDefined component, with a link to the documentation - // on how to define resources within the Application CR. - if (application.getResourcesList().length === 0) { - return ( - - ); - } - - return ( - - {application.getResourcesList().map((resource, i) => ( -
- {resource.getKindsList().map((kind, j) => ( - - toggle(`resources-accordion-${i}-${j}`)} - isExpanded={expanded.includes(`resources-accordion-${i}-${j}`)} - id={`resources-toggle-${i}-${j}`} - > - {resources[kind].title} - - - - - - ))} -
- ))} -
- ); -}; - -export default Resources; diff --git a/app/src/components/applications/overview/Card.tsx b/app/src/components/applications/overview/Card.tsx deleted file mode 100644 index 736ac7410..000000000 --- a/app/src/components/applications/overview/Card.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { CardBody, CardTitle, Card as PatternflyCard } from '@patternfly/react-core'; -import React, { useCallback, useEffect, useState } from 'react'; - -import { - DatasourceMetrics, - DatasourceOptions, - GetMetricsRequest, - GetMetricsResponse, -} from 'generated/proto/datasources_pb'; -import { Application } from 'generated/proto/application_pb'; -import Chart from 'components/applications/overview/Chart'; -import { DatasourcesPromiseClient } from 'generated/proto/datasources_grpc_web_pb'; -import { apiURL } from 'utils/constants'; - -const datasourcesService = new DatasourcesPromiseClient(apiURL, null, null); - -interface ICardProps { - application: Application; - select: (application: Application) => void; -} - -// Card displays a single application within the Applications gallery. The component requires the application and a -// select function as props. The select function is called, when the user clicks on the card. In the applications pages, -// this will then open the drawer with the selected application. -const Card: React.FunctionComponent = ({ application, select }: ICardProps) => { - const metrics = application.getMetrics(); - - const [data, setData] = useState([]); - const [error, setError] = useState(''); - - // fetchData is used to fetch the data for the health metric of an Application. The data is then displayed as - // sparkline within the card for the application in the gallery. - const fetchData = useCallback(async () => { - try { - if (metrics && metrics.hasHealth()) { - const options = new DatasourceOptions(); - options.setTimeend(Math.floor(Date.now() / 1000)); - options.setTimestart(Math.floor(Date.now() / 1000) - 3600); - - const getMetricsRequest = new GetMetricsRequest(); - getMetricsRequest.setName(metrics.getDatasource()); - getMetricsRequest.setOptions(options); - - const health = metrics.getHealth(); - if (health) { - getMetricsRequest.setQueriesList(health.getQueriesList()); - } - - const getMetricsResponse: GetMetricsResponse = await datasourcesService.getMetrics(getMetricsRequest, null); - - setData(getMetricsResponse.getMetricsList()); - setError(''); - } - } catch (err) { - setError(err.message); - } - }, [metrics]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - // The card for an application can have three different state. When an error occured during the data fetch, we display - // this error on the card. When all data was successfully loaded we display the sparkline. If the application doesn't - // define a health chart, we just display the namespace and cluster for the application in the card body. - return ( - select(application)}> - {application.getName()} - {error ? ( - Could not get health data: {error} - ) : data.length > 0 && metrics && metrics.hasHealth() ? ( - - - - ) : ( - -
Namespace: {application.getNamespace()}
-
Cluster: {application.getCluster()}
-
- )} -
- ); -}; - -export default Card; diff --git a/app/src/components/applications/overview/Chart.tsx b/app/src/components/applications/overview/Chart.tsx deleted file mode 100644 index bd01b75fb..000000000 --- a/app/src/components/applications/overview/Chart.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { ChartArea, ChartGroup } from '@patternfly/react-charts'; -import React, { useEffect, useRef, useState } from 'react'; - -import { DatasourceMetrics } from 'generated/proto/datasources_pb'; - -export interface IChartProps { - title?: string; - unit?: string; - metrics: DatasourceMetrics[]; -} - -// Chart is used to render a sparkline on the card of an application in the overview gallery. Above the sparkline we -// show the last/current value of the metric and the chart title. -const Chart: React.FunctionComponent = ({ title, unit, metrics }: IChartProps) => { - const refChart = useRef(null); - const [width, setWidth] = useState(0); - const [height, setHeight] = useState(0); - - // useEffect is executed on every render of this component. This is needed, so that we are able to use a width of 100% - // and a static height for the chart. - useEffect(() => { - if (refChart && refChart.current) { - setWidth(refChart.current.getBoundingClientRect().width); - setHeight(refChart.current.getBoundingClientRect().height); - } - }, []); - - return ( - -
- {metrics[0].getDataList()[metrics[0].getDataList().length - 1].getY().toFixed(2)} {unit ? unit : ''} -
- {title ? ( -
{title}
- ) : null} - -
- - {metrics.map((metric, index) => ( - - ))} - -
-
- ); -}; - -export default Chart; diff --git a/app/src/components/datasources/Datasource.tsx b/app/src/components/datasources/Datasource.tsx deleted file mode 100644 index d84a62043..000000000 --- a/app/src/components/datasources/Datasource.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Alert, AlertActionLink, AlertVariant, PageSection, PageSectionVariants } from '@patternfly/react-core'; -import { useHistory, useParams } from 'react-router-dom'; -import React from 'react'; - -import Elasticsearch from 'components/datasources/elasticsearch/Elasticsearch'; -import Prometheus from 'components/datasources/prometheus/Prometheus'; - -interface IDatasourceParams { - type: string; - name: string; -} - -// Datasource is the component, which checks the provided type from the URL and renders the corresponding component for -// the datasource type. -const Datasource: React.FunctionComponent = () => { - const params = useParams(); - const history = useHistory(); - - const goToDatasources = (): void => { - history.push('/'); - }; - - if (params.type === 'prometheus') { - return ; - } - - if (params.type === 'elasticsearch') { - return ; - } - - // When the provided datasource type, isn't valid, the user will see the following error, with an action to go back to - // the datasource page. - return ( - - - Datasources - - } - /> - - ); -}; - -export default Datasource; diff --git a/app/src/components/datasources/Datasources.tsx b/app/src/components/datasources/Datasources.tsx deleted file mode 100644 index 1ba3ec33e..000000000 --- a/app/src/components/datasources/Datasources.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { - Alert, - AlertActionLink, - AlertVariant, - Gallery, - GalleryItem, - PageSection, - PageSectionVariants, - Title, -} from '@patternfly/react-core'; -import React, { useCallback, useEffect, useState } from 'react'; -import { useHistory } from 'react-router-dom'; - -import { GetDatasourcesRequest, GetDatasourcesResponse } from 'generated/proto/datasources_pb'; -import { Datasource } from 'generated/proto/datasources_pb'; -import { DatasourcesPromiseClient } from 'generated/proto/datasources_grpc_web_pb'; -import Item from 'components/datasources/Item'; -import { apiURL } from 'utils/constants'; -import { datasourcesDescription } from 'utils/constants'; - -const datasourcesService = new DatasourcesPromiseClient(apiURL, null, null); - -// Datasources renders a gallery with all configured datasources. A click on the card of a datasource redirects the user -// to the page for this datasource. -const Datasources: React.FunctionComponent = () => { - const history = useHistory(); - const [datasources, setDatasources] = useState([]); - const [error, setError] = useState(''); - - // In case of an error the user can retry the API call or he can go back to the overview page. - const goToOverview = (): void => { - history.push('/'); - }; - - // fetchDatasources fetches all datasources from the gRPC API. When an error occurs during the API call, the user will - // see the error and he can retry the call. - const fetchDatasources = useCallback(async () => { - try { - const getDatasourcesRequest = new GetDatasourcesRequest(); - - const getDatasourcesResponse: GetDatasourcesResponse = await datasourcesService.getDatasources( - getDatasourcesRequest, - null, - ); - - setError(''); - setDatasources(getDatasourcesResponse.getDatasourcesList()); - } catch (err) { - setError(err.message); - } - }, []); - - useEffect(() => { - fetchDatasources(); - }, [fetchDatasources]); - - // If there is an error, we will show it to the user. The user then has the option to retry the failed API call or to - // go to the overview page. - if (error) { - return ( - - - Retry - Overview - - } - > -

{error}

-
-
- ); - } - - return ( - - - - Datasources - -

{datasourcesDescription}

-
- - - - {datasources.map((datasource, index) => ( - - - - ))} - - -
- ); -}; - -export default Datasources; diff --git a/app/src/components/datasources/Item.tsx b/app/src/components/datasources/Item.tsx deleted file mode 100644 index 64dea8bef..000000000 --- a/app/src/components/datasources/Item.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Card, CardBody, CardTitle } from '@patternfly/react-core'; -import React from 'react'; -import { useHistory } from 'react-router-dom'; - -// ILogos is the interface for the datasource logos. -interface ILogos { - [key: string]: string; -} - -// logos is an object, with the datasource types as key and the image path for that datasource as logo. -const logos: ILogos = { - elasticsearch: '/img/datasources/elasticsearch.png', - prometheus: '/img/datasources/prometheus.png', -}; - -interface IItemProps { - name: string; - type: string; - link: string; -} - -// Item is a single datasource for the gallery in the datasources page. The datasource is presented inside a Card -// component. The card contains the configured name of the datasource, a link to the corresponding datasource page and -// the brand icon for the datasource. -const Item: React.FunctionComponent = ({ name, type, link }: IItemProps) => { - const history = useHistory(); - - const handleClick = (): void => { - history.push(link); - }; - - return ( - - {name} - - {type} - - - ); -}; - -export default Item; diff --git a/app/src/components/datasources/elasticsearch/Buckets.tsx b/app/src/components/datasources/elasticsearch/Buckets.tsx deleted file mode 100644 index 8839931a5..000000000 --- a/app/src/components/datasources/elasticsearch/Buckets.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Card, CardBody, CardTitle } from '@patternfly/react-core'; -import { - Chart, - ChartAxis, - ChartBar, - ChartLegendTooltip, - ChartThemeColor, - createContainer, -} from '@patternfly/react-charts'; -import React, { useEffect, useRef, useState } from 'react'; - -import { DatasourceLogsBucket } from 'generated/proto/datasources_pb'; -import { formatTime } from 'utils/helpers'; - -interface ILabels { - datum: DatasourceLogsBucket.AsObject; -} - -export interface IBucketsProps { - hits: number; - took: number; - buckets: DatasourceLogsBucket.AsObject[]; -} - -// Buckets renders a bar chart with the distribution of the number of logs accross the selected time range. -const Buckets: React.FunctionComponent = ({ hits, took, buckets }: IBucketsProps) => { - const refChart = useRef(null); - const [width, setWidth] = useState(0); - const [height, setHeight] = useState(0); - - // useEffect is executed on every render of this component. This is needed, so that we are able to use a width of 100% - // and a static height for the chart. - useEffect(() => { - if (refChart && refChart.current) { - setWidth(refChart.current.getBoundingClientRect().width); - setHeight(refChart.current.getBoundingClientRect().height); - } - }, []); - - const CursorVoronoiContainer = createContainer('voronoi', 'cursor'); - const legendData = [{ childName: 'count', name: 'Number of Documents' }]; - - return ( - - - {hits} Documents in {took} Milliseconds - - -
- `${datum.y}`} - labelComponent={ - formatTime(Math.floor(point.x / 1000))} - /> - } - mouseFollowTooltips - voronoiDimension="x" - voronoiPadding={0} - /> - } - height={height} - legendData={legendData} - legendPosition={undefined} - padding={{ bottom: 30, left: 0, right: 0, top: 0 }} - scale={{ x: 'time', y: 'linear' }} - themeColor={ChartThemeColor.multiOrdered} - width={width} - > - - - -
-
-
- ); -}; - -export default Buckets; diff --git a/app/src/components/datasources/elasticsearch/Document.tsx b/app/src/components/datasources/elasticsearch/Document.tsx deleted file mode 100644 index 3e5a3add6..000000000 --- a/app/src/components/datasources/elasticsearch/Document.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { - DrawerActions, - DrawerCloseButton, - DrawerHead, - DrawerPanelBody, - DrawerPanelContent, -} from '@patternfly/react-core'; -import React, { useEffect, useRef } from 'react'; -import { highlightBlock, registerLanguage } from 'highlight.js'; -import json from 'highlight.js/lib/languages/json'; - -import 'highlight.js/styles/nord.css'; - -import { IDocument, formatTimeWrapper } from 'components/datasources/elasticsearch/helpers'; -import Title from 'components/shared/Title'; - -registerLanguage('json', json); - -export interface IDocumentProps { - document: IDocument; - close: () => void; -} - -// Document renders a single document in a drawer panel. We show the whole JSON representation for this document in a -// code view. The highlighting of this JSON document is handled by highlight.js. -const Document: React.FunctionComponent = ({ document, close }: IDocumentProps) => { - const code = useRef(null); - - // useEffect apply the highlighting to the given JSON document. - useEffect(() => { - if (code.current) { - highlightBlock(code.current); - } - }); - - return ( - - - - <DrawerActions className="kobs-drawer-actions"> - <DrawerCloseButton onClose={close} /> - </DrawerActions> - </DrawerHead> - - <DrawerPanelBody className="kobs-drawer-panel-body"> - <pre className="pf-u-pb-md"> - <code ref={code}>{JSON.stringify(document, null, 2)}</code> - </pre> - </DrawerPanelBody> - </DrawerPanelContent> - ); -}; - -export default Document; diff --git a/app/src/components/datasources/elasticsearch/Documents.tsx b/app/src/components/datasources/elasticsearch/Documents.tsx deleted file mode 100644 index 820ff3f62..000000000 --- a/app/src/components/datasources/elasticsearch/Documents.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { TableComposable, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; -import React from 'react'; - -import { IDocument, formatTimeWrapper, getProperty } from 'components/datasources/elasticsearch/helpers'; - -export interface IDocumentsProps { - selectedFields: string[]; - documents: IDocument[]; - select?: (doc: IDocument) => void; -} - -// Documents renders a list of documents. If the user has selected some fields, we will render the table with the -// selected fields. If the selected fields list is empty, we only render the @timestamp field and the _source field as -// the only two columns -const Documents: React.FunctionComponent<IDocumentsProps> = ({ - selectedFields, - documents, - select, -}: IDocumentsProps) => { - return ( - <div className="kobsis-table-wrapper"> - <TableComposable aria-label="Logs" variant={TableVariant.compact} borders={false}> - <Thead> - <Tr> - <Th>Time</Th> - {selectedFields.length > 0 ? ( - selectedFields.map((selectedField, index) => <Th key={index}>{selectedField}</Th>) - ) : ( - <Th>_source</Th> - )} - </Tr> - </Thead> - <Tbody> - {documents.map((document, index) => ( - <Tr key={index} onClick={select ? (): void => select(document) : undefined}> - <Td dataLabel="Time">{formatTimeWrapper(document['_source']['@timestamp'])}</Td> - {selectedFields.length > 0 ? ( - selectedFields.map((selectedField, index) => ( - <Td key={index} dataLabel={selectedField}> - {getProperty(document['_source'], selectedField)} - </Td> - )) - ) : ( - <Td dataLabel="_source">{JSON.stringify(document['_source'])}</Td> - )} - </Tr> - ))} - </Tbody> - </TableComposable> - </div> - ); -}; - -export default Documents; diff --git a/app/src/components/datasources/elasticsearch/Elasticsearch.tsx b/app/src/components/datasources/elasticsearch/Elasticsearch.tsx deleted file mode 100644 index 88d858f62..000000000 --- a/app/src/components/datasources/elasticsearch/Elasticsearch.tsx +++ /dev/null @@ -1,279 +0,0 @@ -import { - Alert, - AlertVariant, - Button, - ButtonVariant, - Drawer, - DrawerContent, - DrawerContentBody, - Grid, - GridItem, - PageSection, - PageSectionVariants, - TextInput, - Title, - Toolbar, - ToolbarContent, - ToolbarGroup, - ToolbarItem, - ToolbarToggleGroup, -} from '@patternfly/react-core'; -import React, { useCallback, useEffect, useState } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; -import FilterIcon from '@patternfly/react-icons/dist/js/icons/filter-icon'; - -import { DatasourceLogsBucket, GetLogsRequest, GetLogsResponse } from 'generated/proto/datasources_pb'; -import { IDocument, getFields } from 'components/datasources/elasticsearch/helpers'; -import { ApplicationLogsQuery } from 'generated/proto/application_pb'; -import Buckets from 'components/datasources/elasticsearch/Buckets'; -import { DatasourcesPromiseClient } from 'generated/proto/datasources_grpc_web_pb'; -import Document from 'components/datasources/elasticsearch/Document'; -import Documents from 'components/datasources/elasticsearch/Documents'; -import Fields from 'components/datasources/elasticsearch/Fields'; -import { IDatasourceOptions } from 'utils/proto'; -import Options from 'components/applications/details/metrics/Options'; -import { apiURL } from 'utils/constants'; -import { convertDatasourceOptionsToProto } from 'utils/proto'; - -const datasourcesService = new DatasourcesPromiseClient(apiURL, null, null); - -// IQueryOptions is the interface for all query options. It extends the existing datasource options interface and adds -// a new query property. -interface IQueryOptions extends IDatasourceOptions { - query: string; - scrollID: string; - selectedFields: string[]; -} - -// parseSearch parses the provided query parameters and returns a query options object. This is needed so that an user -// can share his current URL with other users. So that this URL must contain all properties provided by the user. -const parseSearch = (search: string): IQueryOptions => { - const params = new URLSearchParams(search); - const fields = params.get('fields'); - - return { - query: params.get('query') ? (params.get('query') as string) : '', - scrollID: params.get('scrollID') ? (params.get('scrollID') as string) : '', - selectedFields: fields ? fields.split(',') : [], - timeEnd: params.get('timeEnd') ? parseInt(params.get('timeEnd') as string) : Math.floor(Date.now() / 1000), - timeStart: params.get('timeStart') - ? parseInt(params.get('timeStart') as string) - : Math.floor(Date.now() / 1000) - 3600, - }; -}; - -export interface IElasticsearchProps { - name: string; -} - -// Elasticsearhc implements the Elasticsearch UI for kobs. It can be used to query a configured Elasticsearch instance -// and show the logs in a table. -const Elasticsearch: React.FunctionComponent<IElasticsearchProps> = ({ name }: IElasticsearchProps) => { - const history = useHistory(); - const location = useLocation(); - const [query, setQuery] = useState<string>(''); - const [hits, setHits] = useState<number>(0); - const [took, setTook] = useState<number>(0); - const [scrollID, setScrollID] = useState<string>(''); - const [options, setOptions] = useState<IDatasourceOptions>(); - const [selectedFields, setSelectedFields] = useState<string[]>([]); - const [fields, setFields] = useState<string[]>([]); - const [documents, setDocuments] = useState<IDocument[]>([]); - const [document, setDocument] = useState<IDocument>(); - const [buckets, setBuckets] = useState<DatasourceLogsBucket.AsObject[]>([]); - const [error, setError] = useState<string>(''); - const [isLoading, setIsLoading] = useState<boolean>(false); - - // load changes the query parameters for the current page, to the user provided values. We change the query - // parameters, instead of directly fetching the logs, so that a user can share his current view with other users. - const load = async (): Promise<void> => { - history.push({ - pathname: location.pathname, - search: `?query=${query}&fields=${selectedFields.join(',')}&timeEnd=${options?.timeEnd}&timeStart=${ - options?.timeStart - }`, - }); - }; - - // loadMore is called, when the user clicks the load more button. Instead to the normal load function we set the - // scroll id as additional query parameter. - const loadMore = async (): Promise<void> => { - history.push({ - pathname: location.pathname, - search: `?query=${query}&fields=${selectedFields.join(',')}&scrollID=${scrollID}&timeEnd=${ - options?.timeEnd - }&timeStart=${options?.timeStart}`, - }); - }; - - // selectField adds the given field to the list of selected fields. - const selectField = (field: string): void => { - setSelectedFields((f) => [...f, field]); - }; - - // unselectField removes the given field from the list of selected fields. - const unselectField = (field: string): void => { - setSelectedFields(selectedFields.filter((f) => f !== field)); - }; - - // fetchLogs call the getLogs function to retrieve the logs for a given query. If the scroll id is present in the - // query options, we are fetching more logs for a query and adding the logs to the documents list. If the scroll id - // isn't present we set the documents to the result list. - // The returned logs are a string, but since we know that this is a Elasticsearch datasource, we can savely parse the - // string into a JSON array. - const fetchLogs = useCallback( - async (queryOptions: IQueryOptions): Promise<void> => { - try { - if (queryOptions.query) { - setIsLoading(true); - const logsQuery = new ApplicationLogsQuery(); - logsQuery.setQuery(queryOptions.query); - - const getLogsRequest = new GetLogsRequest(); - getLogsRequest.setName(name); - getLogsRequest.setScrollid(queryOptions.scrollID); - getLogsRequest.setOptions(convertDatasourceOptionsToProto(queryOptions)); - getLogsRequest.setQuery(logsQuery); - - const getLogsResponse: GetLogsResponse = await datasourcesService.getLogs(getLogsRequest, null); - - if (queryOptions.scrollID === '') { - const parsed = JSON.parse(getLogsResponse.getLogs()); - setFields(getFields(parsed.slice(parsed.length > 10 ? 10 : parsed.length))); - setDocuments(parsed); - } else { - setDocuments((d) => [...d, ...JSON.parse(getLogsResponse.getLogs())]); - } - - if (getLogsResponse.toObject().bucketsList.length > 0) { - setBuckets(getLogsResponse.toObject().bucketsList); - } - - setHits(getLogsResponse.getHits()); - setTook(getLogsResponse.getTook()); - setScrollID(getLogsResponse.getScrollid()); - setIsLoading(false); - setError(''); - } - } catch (err) { - setIsLoading(false); - setError(err.message); - } - }, - [name], - ); - - // useEffect is called every time, when the query parameters for the current location are changing. Then we parse the - // query parameters, setting our states to the new values and finally we are calling the fetch logs function. - useEffect(() => { - const queryOptions = parseSearch(location.search); - setQuery(queryOptions.query); - setSelectedFields(queryOptions.selectedFields); - setOptions(queryOptions); - fetchLogs(queryOptions); - }, [fetchLogs, location.search]); - - return ( - <React.Fragment> - <PageSection variant={PageSectionVariants.light}> - <Title headingLevel="h6" size="xl"> - {name} - - - - - } breakpoint="lg"> - - - setQuery(value)} - /> - - {options ? ( - - setOptions(opts)} /> - - ) : null} - - - - - - - - - - - setDocument(undefined)} /> : undefined - } - > - - - {error ? ( - -

{error}

-
- ) : ( - - - - {fields.length > 0 || selectedFields.length > 0 ? ( - - ) : null} - - - {buckets.length > 0 ? : null} - -

 

- - {documents.length > 0 ? ( - setDocument(doc)} - /> - ) : null} - -

 

- - {scrollID !== '' ? ( - - ) : null} -
-
-
- )} -
-
-
-
- - ); -}; - -export default Elasticsearch; diff --git a/app/src/components/datasources/elasticsearch/Fields.tsx b/app/src/components/datasources/elasticsearch/Fields.tsx deleted file mode 100644 index b089c8694..000000000 --- a/app/src/components/datasources/elasticsearch/Fields.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { SimpleList, SimpleListItem } from '@patternfly/react-core'; -import React from 'react'; - -export interface IFieldsProps { - fields: string[]; - selectedFields: string[]; - selectField: (field: string) => void; - unselectField: (field: string) => void; -} - -// Fields is used to show the list of parsed and selected fields. When a user selects a field from the fields list, this -// field is added to the list of selected fields. When the user selects a field from the selected fields list this field -// will be removed from this list. -const Fields: React.FunctionComponent = ({ - fields, - selectedFields, - selectField, - unselectField, -}: IFieldsProps) => { - return ( - - {selectedFields.length > 0 ?

Selected Fields

: null} - - {selectedFields.length > 0 ? ( - - {selectedFields.map((selectedField, index) => ( - unselectField(selectedField)} isActive={false}> - {selectedField} - - ))} - - ) : null} - - {fields.length > 0 ?

Fields

: null} - - {fields.length > 0 ? ( - - {fields.map((field, index) => ( - selectField(field)} isActive={false}> - {field} - - ))} - - ) : null} -
- ); -}; - -export default Fields; diff --git a/app/src/components/datasources/elasticsearch/helpers.ts b/app/src/components/datasources/elasticsearch/helpers.ts deleted file mode 100644 index 385ec39ef..000000000 --- a/app/src/components/datasources/elasticsearch/helpers.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { formatTime } from 'utils/helpers'; - -// IDocument is the interface for a single Elasticsearch document. It is just an general interface for the JSON -// representation of this document. -export interface IDocument { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -// getFieldsRecursively returns the fields for a single document as a list of string. -export const getFieldsRecursively = (prefix: string, document: IDocument): string[] => { - const fields: string[] = []; - for (const field in document) { - if (typeof document[field] === 'object') { - fields.push(...getFieldsRecursively(prefix ? `${prefix}.${field}` : field, document[field])); - } else { - fields.push(prefix ? `${prefix}.${field}` : field); - } - } - - return fields; -}; - -// getFields is used to get all fields as strings for the given documents. To get the fields we are looping over the -// given documents and adding each field from this document. As a last step we have to remove all duplicated fields. -export const getFields = (documents: IDocument[]): string[] => { - const fields: string[] = []; - for (const document of documents) { - fields.push(...getFieldsRecursively('', document['_source'])); - } - - const uniqueFields: string[] = []; - for (const field of fields) { - if (uniqueFields.indexOf(field) === -1) { - uniqueFields.push(field); - } - } - - return uniqueFields; -}; - -// getProperty returns the property of an object for a given key. -// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types -export const getProperty = (obj: any, key: string): string | number => { - return key.split('.').reduce((o, x) => { - return typeof o == 'undefined' || o === null ? o : o[x]; - }, obj); -}; - -// formatTimeWrapper is a wrapper for our shared formatTime function. It is needed to convert a given time string to the -// corresponding timestamp representation, which we need for the formatTime function. -export const formatTimeWrapper = (time: string): string => { - return formatTime(Math.floor(new Date(time).getTime() / 1000)); -}; diff --git a/app/src/components/datasources/prometheus/Data.tsx b/app/src/components/datasources/prometheus/Data.tsx deleted file mode 100644 index 7e3c5b1ab..000000000 --- a/app/src/components/datasources/prometheus/Data.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { - Card, - CardBody, - Flex, - FlexItem, - SimpleList, - SimpleListItem, - ToggleGroup, - ToggleGroupItem, -} from '@patternfly/react-core'; -import React, { useState } from 'react'; - -import { DatasourceMetrics } from 'generated/proto/datasources_pb'; -import DefaultChart from 'components/applications/details/metrics/charts/Default'; - -interface IDataProps { - data: DatasourceMetrics[]; -} - -// Data is used to render the fetched time series, for a user provided query. By default the corresponding chart will -// render all loaded time series. When the user selects a specif time series, the chart will only render this series. -// A user can also decided, how he wants to see his data: As line vs. area chart or unstacked vs. stacked. -const Data: React.FunctionComponent = ({ data }: IDataProps) => { - const [type, setType] = useState('line'); - const [stacked, setStacked] = useState(false); - const [selectedData, setSelectedData] = useState([]); - - const select = (series: DatasourceMetrics): void => { - if (selectedData.length === 1 && selectedData[0].getLabel() === series.getLabel()) { - setSelectedData(data); - } else { - setSelectedData([series]); - } - }; - - if (data.length === 0) { - return null; - } - - return ( - - - - - - setType('line')} /> - setType('area')} /> - - - - - setStacked(false)} /> - setStacked(true)} /> - - - - -

 

- - - -

 

- - - {data.map((series, index) => ( - select(series)} - isActive={selectedData.length === 1 && selectedData[0].getLabel() === series.getLabel()} - > - {series.getLabel()} - {series.getDataList()[series.getDataList().length - 1].getY()} - - ))} - -
-
- ); -}; - -export default Data; diff --git a/app/src/components/datasources/prometheus/Prometheus.tsx b/app/src/components/datasources/prometheus/Prometheus.tsx deleted file mode 100644 index f37bedda5..000000000 --- a/app/src/components/datasources/prometheus/Prometheus.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { - Alert, - AlertVariant, - Button, - ButtonVariant, - PageSection, - PageSectionVariants, - TextArea, - Title, - Toolbar, - ToolbarContent, - ToolbarGroup, - ToolbarItem, - ToolbarToggleGroup, -} from '@patternfly/react-core'; -import React, { useCallback, useEffect, useState } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; -import FilterIcon from '@patternfly/react-icons/dist/js/icons/filter-icon'; - -import { DatasourceMetrics, GetMetricsRequest, GetMetricsResponse } from 'generated/proto/datasources_pb'; -import { ApplicationMetricsQuery } from 'generated/proto/application_pb'; -import Data from 'components/datasources/prometheus/Data'; -import { DatasourcesPromiseClient } from 'generated/proto/datasources_grpc_web_pb'; -import { IDatasourceOptions } from 'utils/proto'; -import Options from 'components/applications/details/metrics/Options'; -import { apiURL } from 'utils/constants'; -import { convertDatasourceOptionsToProto } from 'utils/proto'; - -const datasourcesService = new DatasourcesPromiseClient(apiURL, null, null); - -// IQueryOptions is the interface for all query options. It extends the existing datasource options interface and adds -// a new query property. -interface IQueryOptions extends IDatasourceOptions { - query: string; -} - -// parseSearch parses the provided query parameters and returns a query options object. This is needed so that an user -// can share his current URL with other users. So that this URL must contain all properties provided by the user. -const parseSearch = (search: string): IQueryOptions => { - const params = new URLSearchParams(search); - return { - query: params.get('query') ? (params.get('query') as string) : '', - resolution: params.get('resolution') ? (params.get('resolution') as string) : '', - timeEnd: params.get('timeEnd') ? parseInt(params.get('timeEnd') as string) : Math.floor(Date.now() / 1000), - timeStart: params.get('timeStart') - ? parseInt(params.get('timeStart') as string) - : Math.floor(Date.now() / 1000) - 3600, - }; -}; - -export interface IPrometheusProps { - name: string; -} - -// Prometheus implements the Prometheus UI for kobs. It can be used to query a configured Prometheus instance and show -// the results in a list and chart. -const Prometheus: React.FunctionComponent = ({ name }: IPrometheusProps) => { - const history = useHistory(); - const location = useLocation(); - const [query, setQuery] = useState(''); - const [options, setOptions] = useState(); - const [data, setData] = useState([]); - const [error, setError] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - // run changes the query parameters for the current page, to the user provided values. We change the query parameters, - // instead of directly fetching the data, so that a user can share his current view with other users. - const run = async (): Promise => { - history.push({ - pathname: location.pathname, - search: `?query=${query}&resolution=${options?.resolution}&timeEnd=${options?.timeEnd}&timeStart=${options?.timeStart}`, - }); - }; - - // fetchData is used to fetch all metrics for the user provided query. We also set the user provided datasource - // options, which are used to set the time range and resolution of the returned time series data. - const fetchData = useCallback( - async (queryOptions: IQueryOptions): Promise => { - try { - if (queryOptions.query) { - setIsLoading(true); - const metricsQuery = new ApplicationMetricsQuery(); - metricsQuery.setQuery(queryOptions.query); - - const getMetricsRequest = new GetMetricsRequest(); - getMetricsRequest.setName(name); - getMetricsRequest.setOptions(convertDatasourceOptionsToProto(queryOptions)); - getMetricsRequest.setQueriesList([metricsQuery]); - - const getMetricsResponse: GetMetricsResponse = await datasourcesService.getMetrics(getMetricsRequest, null); - - setData(getMetricsResponse.getMetricsList()); - setIsLoading(false); - setError(''); - } - } catch (err) { - setIsLoading(false); - setError(err.message); - } - }, - [name], - ); - - // useEffect is executed every time the query parameters (location.search) are changing. When the parameters are - // changed we are pasing the query string, setting the corresponding query and options variable and then we are - // fetching the data. - useEffect(() => { - const queryOptions = parseSearch(location.search); - setQuery(queryOptions.query); - setOptions(queryOptions); - fetchData(queryOptions); - }, [fetchData, location.search]); - - return ( - - - - {name} - - - - - } breakpoint="lg"> - - -