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: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
- [#32](https://github.com/kobsio/kobs/pull/32): Add support for container logs via the Kubernetes API.
- [#34](https://github.com/kobsio/kobs/pull/34): Add a new Custom Resource Definition for Teams. Teams can be used to define the ownership for Applications and other Kubernetes resources. :warning: *Breaking change:* :warning: We are now using the `apiextensions.k8s.io/v1` API for the Custom Resource Definitions of kobs.
- [#39](https://github.com/kobsio/kobs/pull/39): Add Opsgenie plugin to view alerts within an Application.
- [#40](https://github.com/kobsio/kobs/pull/39): Prometheus plugin metric name suggestions.
- [#40](https://github.com/kobsio/kobs/pull/40): Add metric name suggestions for Prometheus plugin.
- [#41](https://github.com/kobsio/kobs/pull/41): Add overview and Pods tab for resource details.

### Fixed

Expand Down
46 changes: 45 additions & 1 deletion app/src/components/resources/ResourceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { Plugin as IPlugin } from 'proto/plugins_grpc_web_pb';
import Plugin from 'components/plugins/Plugin';
import ResourceEvents from 'components/resources/ResourceEvents';
import ResourceLogs from 'components/resources/ResourceLogs';
import ResourceOverview from 'components/resources/ResourceOverview';
import ResourcePods from 'components/resources/ResourcePods';
import Title from 'components/Title';
import { plugins as pluginsDefinition } from 'utils/plugins';

Expand Down Expand Up @@ -65,6 +67,27 @@ const interpolate = (str: string, resource: any, interpolator: string[] = ['<<',
.join('');
};

// getSelector is used to get the label selector for various resources as string. The returned string can be used in
// a Kubernetes API request to get the all pods, which are matching the label selector.
const getSelector = (resource: IRow): string => {
if (resource.props && resource.props.apiVersion && resource.props.kind) {
if (
(resource.props.apiVersion === 'apps/v1' && resource.props.kind === 'Deployment') ||
(resource.props.apiVersion === 'apps/v1' && resource.props.kind === 'DaemonSet') ||
(resource.props.apiVersion === 'apps/v1' && resource.props.kind === 'StatefulSet') ||
(resource.props.apiVersion === 'batch/v1' && resource.props.kind === 'Job')
) {
return resource.props?.spec?.selector?.matchLabels
? Object.keys(resource.props.spec.selector.matchLabels)
.map((key) => `${key}=${resource.props.spec.selector.matchLabels[key]}`)
.join(',')
: '';
}
}

return '';
};

interface IApplications {
namespace?: string;
name: string;
Expand All @@ -84,9 +107,11 @@ const ResourceDetails: React.FunctionComponent<IResourceDetailsProps> = ({
resource,
close,
}: IResourceDetailsProps) => {
const [activeTab, setActiveTab] = useState<string>('yaml');
const [activeTab, setActiveTab] = useState<string>('overview');
const pluginsContext = useContext<IPluginsContext>(PluginsContext);

const podSelector = getSelector(resource);

let applications: IApplications[] = [];
let teams: string[] = [];
const plugins: IPlugin.AsObject[] = [];
Expand Down Expand Up @@ -226,13 +251,20 @@ const ResourceDetails: React.FunctionComponent<IResourceDetailsProps> = ({
isFilled={true}
mountOnEnter={true}
>
<Tab eventKey="overview" title={<TabTitleText>Overview</TabTitleText>}>
<div style={{ maxWidth: '100%', overflowX: 'scroll', padding: '24px 24px' }}>
<ResourceOverview resource={resource} />
</div>
</Tab>

<Tab eventKey="yaml" title={<TabTitleText>Yaml</TabTitleText>}>
<div style={{ maxWidth: '100%', overflowX: 'scroll', padding: '24px 24px' }}>
<Card>
<Editor value={yaml.dump(resource.props)} mode="yaml" readOnly={true} />
</Card>
</div>
</Tab>

<Tab eventKey="events" title={<TabTitleText>Events</TabTitleText>}>
<div style={{ maxWidth: '100%', overflowX: 'scroll', padding: '24px 24px' }}>
<ResourceEvents
Expand All @@ -256,6 +288,18 @@ const ResourceDetails: React.FunctionComponent<IResourceDetailsProps> = ({
</Tab>
) : null}

{podSelector ? (
<Tab eventKey="pods" title={<TabTitleText>Pods</TabTitleText>}>
<div style={{ maxWidth: '100%', overflowX: 'scroll', padding: '24px 24px' }}>
<ResourcePods
cluster={resource.cluster.title}
namespace={resource.namespace ? resource.namespace.title : ''}
selector={podSelector}
/>
</div>
</Tab>
) : null}

{pluginsError ? (
<Tab eventKey="plugins" title={<TabTitleText>Plugins</TabTitleText>}>
<div style={{ maxWidth: '100%', overflowX: 'scroll', padding: '24px 24px' }}>
Expand Down
152 changes: 152 additions & 0 deletions app/src/components/resources/ResourceOverview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {
Card,
CardBody,
DescriptionList,
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
} from '@patternfly/react-core';
import { IRow } from '@patternfly/react-table';
import React from 'react';
import { V1OwnerReference } from '@kubernetes/client-node';

import Conditions from 'components/resources/overview/Conditions';
import CronJob from 'components/resources/overview/CronJob';
import DaemonSet from 'components/resources/overview/DaemonSet';
import Deployment from 'components/resources/overview/Deployment';
import Job from 'components/resources/overview/Job';
import Pod from 'components/resources/overview/Pod';
import StatefulSet from 'components/resources/overview/StatefulSet';
import { timeDifference } from 'utils/helpers';

interface IResourceOverviewProps {
resource: IRow;
}

// ResourceOverview is the overview tab for a resource. It shows the metadata of a resource in a clear way. We can also
// add some additional fields on a per resource basis.
const ResourceOverview: React.FunctionComponent<IResourceOverviewProps> = ({ resource }: IResourceOverviewProps) => {
// additions contains a React component with additional details for a resource. The default component just renders the
// conditions of a resource.
let additions =
resource.props && resource.props.status && resource.props.status.conditions ? (
<Conditions conditions={resource.props.status.conditions} />
) : null;

// Overwrite the additions for several resources.
if (resource.props && resource.props.apiVersion && resource.props.kind) {
if (resource.props.apiVersion === 'v1' && resource.props.kind === 'Pod') {
additions = <Pod pod={resource.props} />;
} else if (resource.props.apiVersion === 'apps/v1' && resource.props.kind === 'Deployment') {
additions = (
<Deployment
cluster={resource.cluster?.title}
namespace={resource.namespace?.title}
deployment={resource.props}
/>
);
} else if (resource.props.apiVersion === 'apps/v1' && resource.props.kind === 'DaemonSet') {
additions = (
<DaemonSet cluster={resource.cluster?.title} namespace={resource.namespace?.title} daemonSet={resource.props} />
);
} else if (resource.props.apiVersion === 'apps/v1' && resource.props.kind === 'StatefulSet') {
additions = (
<StatefulSet
cluster={resource.cluster?.title}
namespace={resource.namespace?.title}
statefulSet={resource.props}
/>
);
} else if (resource.props.apiVersion === 'batch/v1beta1' && resource.props.kind === 'CronJob') {
additions = <CronJob cronJob={resource.props} />;
} else if (resource.props.apiVersion === 'batch/v1' && resource.props.kind === 'Job') {
additions = <Job cluster={resource.cluster?.title} namespace={resource.namespace?.title} job={resource.props} />;
}
}

return (
<Card isCompact={true}>
<CardBody>
<DescriptionList isHorizontal={true}>
{resource.name?.title && (
<DescriptionListGroup>
<DescriptionListTerm>Name</DescriptionListTerm>
<DescriptionListDescription>{resource.name?.title}</DescriptionListDescription>
</DescriptionListGroup>
)}
{resource.namespace?.title && (
<DescriptionListGroup>
<DescriptionListTerm>Namespace</DescriptionListTerm>
<DescriptionListDescription>{resource.namespace?.title}</DescriptionListDescription>
</DescriptionListGroup>
)}
{resource.cluster?.title && (
<DescriptionListGroup>
<DescriptionListTerm>Cluster</DescriptionListTerm>
<DescriptionListDescription>{resource.cluster?.title}</DescriptionListDescription>
</DescriptionListGroup>
)}
{resource.props?.metadata?.labels && (
<DescriptionListGroup>
<DescriptionListTerm>Labels</DescriptionListTerm>
<DescriptionListDescription>
{Object.keys(resource.props?.metadata?.labels).map((key) => (
<div key={key} className="pf-c-chip pf-u-mr-md pf-u-mb-sm" style={{ maxWidth: '100%' }}>
<span className="pf-c-chip__text" style={{ maxWidth: '100%' }}>
{key}: {resource.props?.metadata?.labels[key]}
</span>
</div>
))}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{resource.props?.metadata?.annotations && (
<DescriptionListGroup>
<DescriptionListTerm>Annotations</DescriptionListTerm>
<DescriptionListDescription>
{Object.keys(resource.props?.metadata?.annotations).map((key) => (
<div key={key} className="pf-c-chip pf-u-mr-md pf-u-mb-sm" style={{ maxWidth: '100%' }}>
<span className="pf-c-chip__text" style={{ maxWidth: '100%' }}>
{key}: {resource.props?.metadata?.annotations[key]}
</span>
</div>
))}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{resource.props?.metadata?.creationTimestamp && (
<DescriptionListGroup>
<DescriptionListTerm>Age</DescriptionListTerm>
<DescriptionListDescription>
{timeDifference(
new Date().getTime(),
new Date(resource.props.metadata.creationTimestamp.toString()).getTime(),
)}
<span className="pf-u-pl-sm pf-u-font-size-sm pf-u-color-400">
({resource.props.metadata.creationTimestamp})
</span>
</DescriptionListDescription>
</DescriptionListGroup>
)}
{resource.props?.metadata?.ownerReferences && (
<DescriptionListGroup>
<DescriptionListTerm>Crontrolled By</DescriptionListTerm>
<DescriptionListDescription>
{resource.props?.metadata?.ownerReferences.map((owner: V1OwnerReference, index: number) => (
<div key={index}>
{owner.kind}
<span className="pf-u-pl-sm pf-u-font-size-sm pf-u-color-400">({owner.name})</span>
</div>
))}
</DescriptionListDescription>
</DescriptionListGroup>
)}

{additions}
</DescriptionList>
</CardBody>
</Card>
);
};

export default ResourceOverview;
76 changes: 76 additions & 0 deletions app/src/components/resources/ResourcePods.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Card, Flex, FlexItem } from '@patternfly/react-core';
import { IRow, Table, TableBody, TableHeader } from '@patternfly/react-table';
import React, { useCallback, useEffect, useState } from 'react';

import { ClustersPromiseClient, GetResourcesRequest, GetResourcesResponse } from 'proto/clusters_grpc_web_pb';
import { emptyState, resources } from 'utils/resources';
import { apiURL } from 'utils/constants';

// clustersService is the Clusters gRPC service, which is used to get a list of pods.
const clustersService = new ClustersPromiseClient(apiURL, null, null);

interface IResourcePodsProps {
cluster: string;
namespace: string;
selector: string;
}

// ResourcePods is the pods tab for various resources. It can be used to show the pods for a deployment, statefulset,
// etc. within the tabs.
const ResourcePods: React.FunctionComponent<IResourcePodsProps> = ({
cluster,
namespace,
selector,
}: IResourcePodsProps) => {
const [pods, setPods] = useState<IRow[]>(emptyState(resources.pods.columns.length, ''));

// fetchPods fetches the pods for the given cluster, namespace and label selector.
const fetchPods = useCallback(async () => {
try {
const getResourcesRequest = new GetResourcesRequest();
getResourcesRequest.setClustersList([cluster]);
getResourcesRequest.setNamespacesList([namespace]);
getResourcesRequest.setPath(resources.pods.path);
getResourcesRequest.setResource(resources.pods.resource);
getResourcesRequest.setParamname('labelSelector');
getResourcesRequest.setParam(selector);

const getResourcesResponse: GetResourcesResponse = await clustersService.getResources(getResourcesRequest, null);
const resourceList = getResourcesResponse.getResourcesList();

if (resourceList.length === 1) {
setPods(resources.pods.rows(resourceList));
} else {
setPods(emptyState(resources.pods.columns.length, ''));
}
} catch (err) {
setPods(emptyState(resources.pods.columns.length, err.message));
}
}, [cluster, namespace, selector]);

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

return (
<Card>
<Flex direction={{ default: 'column' }}>
<FlexItem>
<Table
aria-label="pods"
variant="compact"
borders={false}
isStickyHeader={false}
cells={resources.pods.columns}
rows={pods}
>
<TableHeader />
<TableBody />
</Table>
</FlexItem>
</Flex>
</Card>
);
};

export default ResourcePods;
Loading