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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ bin

# Temporary files for code generation
tmp

# application.yaml which contains a list of Application CRs for local testing.
application.yaml
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan

### Added

- [#4](https://github.com/kobsio/kobs/pull/4): Add Custom Resource Definition for Applications.

### Fixed

- [#1](https://github.com/kobsio/kobs/pull/1): Fix mobile layout for the cluster and namespace filter by using a Toolbar instead of FlexItems.
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ chmod +x /usr/local/bin/protoc-gen-grpc-web
go get google.golang.org/protobuf/cmd/protoc-gen-go@v1.25.0
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1.0
go get istio.io/tools/cmd/protoc-gen-deepcopy@v0.0.0-20210206003203-61eabd18b4e0
```

- Install the Kubernetes [code-generator](https://github.com/kubernetes/code-generator) into your `GOPATH`:

```sh
go get k8s.io/code-generator@v0.20.2
```

Expand Down
19 changes: 13 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,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/applications.proto
@rm -rf ./pkg/generated/proto/clusters_deepcopy.gen.go
@rm -rf ./pkg/generated/proto/applications_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/applications_deepcopy.gen.go ./pkg/generated/proto
@rm -rf ./pkg/generated/proto/github.com

.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/apis application:v1alpha1 --output-base ./tmp
@rm -rf ./pkg/apis/application/v1alpha1/zz_generated.deepcopy.go
@rm -rf ./pkg/generated
@mv ./tmp/github.com/kobsio/kobs/pkg/apis/application/v1alpha1/zz_generated.deepcopy.go ./pkg/apis/application/v1alpha1
@mv ./tmp/github.com/kobsio/kobs/pkg/generated ./pkg
@${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
@rm -rf ./tmp
-controller-gen "crd:trivialVersions=true" paths="./..." output:crd:artifacts:config=deploy/crds
-controller-gen "crd:trivialVersions=true" paths="./..." output:crd:artifacts:config=deploy/kustomize/crds

.PHONY: release-major
release-major:
Expand Down
11 changes: 10 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-use-before-define": "off"
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/naming-convention": [
"error",
{"format": ["camelCase"], "selector": "default", "leadingUnderscore": "forbid", "trailingUnderscore": "forbid"},
{"format": ["camelCase", "UPPER_CASE", "PascalCase"], "selector": "variable", "leadingUnderscore": "forbid", "trailingUnderscore": "forbid"},
{"format": ["camelCase", "UPPER_CASE", "PascalCase"], "selector": "property", "leadingUnderscore": "forbid", "trailingUnderscore": "forbid"},
{"format": ["PascalCase"], "selector": "interface", "leadingUnderscore": "forbid", "trailingUnderscore": "forbid", "prefix": ["I"]},
{"format": ["PascalCase"], "selector": "typeAlias", "leadingUnderscore": "forbid", "trailingUnderscore": "forbid"},
{"format": ["PascalCase"], "selector": "typeParameter", "leadingUnderscore": "forbid", "trailingUnderscore": "forbid"}
]
}
},
"prettier": {
Expand Down
21 changes: 12 additions & 9 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@ import '@patternfly/react-core/dist/styles/base.css';
import '@patternfly/patternfly/patternfly.css';
import '@patternfly/patternfly/patternfly-addons.css';

import Applications from './components/applications/Applications';
import Logo from './components/menu/Logo';
import Overview from './components/overview/Overview';
import Resources from './components/resources/Resources';
import Application from 'components/applications/Application';
import Applications from 'components/applications/Applications';
import HeaderLogo from 'components/shared/HeaderLogo';
import Overview from 'components/overview/Overview';
import Resources from 'components/resources/Resources';

import './app.css';
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 = <PageHeader logo={<Logo />} />;
const Header = <PageHeader logo={<HeaderLogo />} />;

return (
<Router>
<Page header={Header}>
<Switch>
<Route exact path="/" component={Overview} />
<Route exact path="/applications" component={Applications} />
<Route exact path="/resources/:kind" component={Resources} />
<Route exact={true} path="/" component={Overview} />
<Route exact={true} path="/applications" component={Applications} />
<Route exact={true} path="/applications/:cluster/:namespace/:name" component={Application} />
<Route exact={true} path="/resources/:kind" component={Resources} />
</Switch>
</Page>
</Router>
Expand Down
8 changes: 5 additions & 3 deletions app/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
padding: 0;
}

/* kobs-code
* Set the maximum width to the width of the parend component and the scrolling bahaviour, when we display code. */
.kobs-code {
/* 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;
}
Expand Down
141 changes: 141 additions & 0 deletions app/src/components/applications/Application.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import {
Alert,
AlertActionLink,
AlertVariant,
List,
ListItem,
ListVariant,
PageSection,
PageSectionVariants,
Tab,
TabContent,
TabTitleText,
Tabs,
} from '@patternfly/react-core';
import { Link, useHistory, useParams } from 'react-router-dom';
import React, { useCallback, useEffect, useRef, useState } from 'react';

import { GetApplicationRequest, GetApplicationResponse } from 'generated/proto/clusters_pb';
import { Application } from 'generated/proto/applications_pb';
import { ClustersPromiseClient } from 'generated/proto/clusters_grpc_web_pb';
import Resources from 'components/applications/details/resources/Resources';
import Title from 'components/shared/Title';
import { apiURL } from 'utils/constants';

const clustersService = new ClustersPromiseClient(apiURL, null, null);

interface IApplicationsParams {
cluster: string;
namespace: string;
name: string;
}

// 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 history = useHistory();
const params = useParams<IApplicationsParams>();
const [application, setApplication] = useState<Application | undefined>(undefined);
const [error, setError] = useState<string>('');
const [activeTabKey, setActiveTabKey] = useState<string>('resources');
const refResourcesContent = useRef<HTMLElement>(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 {
const getApplicationRequest = new GetApplicationRequest();
getApplicationRequest.setCluster(params.cluster);
getApplicationRequest.setNamespace(params.namespace);
getApplicationRequest.setName(params.name);

const getApplicationsResponse: GetApplicationResponse = await clustersService.getApplication(
getApplicationRequest,
null,
);

setError('');
setApplication(getApplicationsResponse.getApplication());
} catch (err) {
setError(err.message);
}
}, [params.cluster, params.namespace, params.name]);

useEffect(() => {
fetchApplication();
}, [fetchApplication]);

if (!application) {
return null;
}

// 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 (
<PageSection variant={PageSectionVariants.default}>
<Alert
variant={AlertVariant.danger}
isInline={false}
title="Application was not found"
actionLinks={
<React.Fragment>
<AlertActionLink onClick={fetchApplication}>Retry</AlertActionLink>
<AlertActionLink onClick={goToOverview}>Overview</AlertActionLink>
</React.Fragment>
}
>
<p>{error}</p>
</Alert>
</PageSection>
);
}

return (
<React.Fragment>
<PageSection variant={PageSectionVariants.light}>
<Title
title={application.getName()}
subtitle={`${application.getNamespace()} (${application.getCluster()})`}
size="xl"
/>
<List variant={ListVariant.inline}>
{application.getLinksList().map((link, index) => (
<ListItem key={index}>
<Link target="_blank" to={link.getLink}>
{link.getTitle()}
</Link>
</ListItem>
))}
</List>
<Tabs
className="pf-u-mt-md"
mountOnEnter={true}
isFilled={true}
activeKey={activeTabKey}
onSelect={(event, tabIndex): void => setActiveTabKey(tabIndex.toString())}
>
<Tab
eventKey="resources"
title={<TabTitleText>Resources</TabTitleText>}
tabContentId="refResources"
tabContentRef={refResourcesContent}
/>
</Tabs>
</PageSection>

<PageSection variant={PageSectionVariants.default}>
<TabContent eventKey={0} id="refResources" ref={refResourcesContent} aria-label="Resources">
<Resources application={application} />
</TabContent>
</PageSection>
</React.Fragment>
);
};

export default Applications;
111 changes: 104 additions & 7 deletions app/src/components/applications/Applications.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,110 @@
import { Gallery, GalleryItem, PageSection, PageSectionVariants } from '@patternfly/react-core';
import React from 'react';
import {
Alert,
AlertVariant,
Drawer,
DrawerContent,
DrawerContentBody,
Gallery,
GalleryItem,
PageSection,
PageSectionVariants,
Title,
} from '@patternfly/react-core';
import React, { useState } from 'react';

import { GetApplicationsRequest, GetApplicationsResponse } from 'generated/proto/clusters_pb';
import { apiURL, applicationsDescription } from 'utils/constants';
import { Application } from 'generated/proto/applications_pb';
import Card from 'components/applications/details/Card';
import { ClustersPromiseClient } from 'generated/proto/clusters_grpc_web_pb';
import DrawerPanel from 'components/applications/details/DrawerPanel';
import Filter from 'components/resources/shared/Filter';

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.
const Applications: React.FunctionComponent = () => {
const [applications, setApplications] = useState<Application[]>([]);
const [selectedApplication, setSelectedApplication] = useState<Application | 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);

const getApplicationsRequest = new GetApplicationsRequest();
getApplicationsRequest.setClustersList(clusters);
getApplicationsRequest.setNamespacesList(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);
}
} catch (err) {
setError(err.message);
setIsLoading(false);
}
};

return (
<PageSection variant={PageSectionVariants.default}>
<Gallery hasGutter={true}>
<GalleryItem>Applications</GalleryItem>
</Gallery>
</PageSection>
<React.Fragment>
<PageSection variant={PageSectionVariants.light}>
<Title headingLevel="h6" size="xl">
Applications
</Title>
<p>{applicationsDescription}</p>
<Filter isLoading={isLoading} onFilter={fetchApplications} />
</PageSection>

<Drawer isExpanded={selectedApplication !== undefined}>
<DrawerContent
panelContent={
selectedApplication ? (
<DrawerPanel application={selectedApplication} close={(): void => setSelectedApplication(undefined)} />
) : undefined
}
>
<DrawerContentBody>
<PageSection className="kobs-drawer-pagesection" variant={PageSectionVariants.default}>
{error ? (
<Alert variant={AlertVariant.danger} isInline={false} title="Could not load applications">
<p>{error}</p>
</Alert>
) : (
<Gallery hasGutter={true}>
{applications.map((application, index) => (
<GalleryItem key={index}>
<Card application={application} select={setSelectedApplication} />
</GalleryItem>
))}
</Gallery>
)}
</PageSection>
</DrawerContentBody>
</DrawerContent>
</Drawer>
</React.Fragment>
);
};

Expand Down
Loading