Allow extensions to register kube-object menus + details (#1108)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2020-10-22 21:41:20 +03:00
1 parent 91bef77997
commit 99db7aca19
101 files changed
+7858 -1604

No files matched your search

@@ -1,12 +0,0 @@
$cert-status-colors: (
ready: $colorOk,
);
@mixin cert-status-bgc {
@each $status, $color in $cert-status-colors {
&.#{$status} {
background: $color;
color: white;
}
}
}
@@ -1,7 +0,0 @@
@import "cert-manager.mixins";
.CertificateDetails {
.Badge {
@include cert-status-bgc;
}
}
@@ -1,142 +0,0 @@
import "./certificate-details.scss"
import React from "react";
import moment from "moment"
import { observer } from "mobx-react";
import { Link } from "react-router-dom";
import { Trans } from "@lingui/macro";
import { DrawerItem, DrawerTitle } from "../../drawer";
import { Badge } from "../../badge";
import { KubeEventDetails } from "../../+events/kube-event-details";
import { KubeObjectDetailsProps } from "../../kube-object";
import { Certificate, certificatesApi } from "../../../api/endpoints/cert-manager.api";
import { cssNames } from "../../../utils";
import { apiManager } from "../../../api/api-manager";
import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
interface Props extends KubeObjectDetailsProps<Certificate> {
}
@observer
export class CertificateDetails extends React.Component<Props> {
render() {
const { object: cert, className } = this.props;
if (!cert) return;
const { spec, status } = cert;
const { acme, isCA, commonName, secretName, dnsNames, duration, ipAddresses, keyAlgorithm, keySize, organization, renewBefore } = spec;
const { lastFailureTime, notAfter } = status;
return (
<div className={cssNames("CertificateDetails", className)}>
<KubeObjectMeta object={cert}/>
<DrawerItem name={<Trans>Issuer</Trans>}>
<Link to={cert.getIssuerDetailsUrl()}>
{cert.getIssuerName()}
</Link>
</DrawerItem>
<DrawerItem name={<Trans>Secret Name</Trans>}>
<Link to={cert.getSecretDetailsUrl()}>
{secretName}
</Link>
</DrawerItem>
<DrawerItem name="CA">
{isCA ? <Trans>Yes</Trans> : <Trans>No</Trans>}
</DrawerItem>
{commonName && (
<DrawerItem name={<Trans>Common Name</Trans>}>
{commonName}
</DrawerItem>
)}
{dnsNames && (
<DrawerItem name={<Trans>DNS names</Trans>} labelsOnly>
{dnsNames.map(name => <Badge key={name} label={name}/>)}
</DrawerItem>
)}
{ipAddresses && (
<DrawerItem name={<Trans>IP addresses</Trans>}>
{ipAddresses.join(", ")}
</DrawerItem>
)}
{organization && (
<DrawerItem name={<Trans>Organization</Trans>}>
{organization.join(", ")}
</DrawerItem>
)}
{duration && (
<DrawerItem name={<Trans>Duration</Trans>}>
{duration}
</DrawerItem>
)}
{renewBefore && (
<DrawerItem name={<Trans>Renew Before</Trans>}>
{renewBefore}
</DrawerItem>
)}
{keySize && (
<DrawerItem name={<Trans>Key Size</Trans>}>
{keySize}
</DrawerItem>
)}
{keyAlgorithm && (
<DrawerItem name={<Trans>Key Algorithm</Trans>}>
{keyAlgorithm}
</DrawerItem>
)}
<DrawerItem name={<Trans>Not After</Trans>}>
{moment(notAfter).format("LLL")}
</DrawerItem>
{lastFailureTime && (
<DrawerItem name={<Trans>Last Failure Time</Trans>}>
{lastFailureTime}
</DrawerItem>
)}
<DrawerItem name={<Trans>Status</Trans>} labelsOnly>
{cert.getConditions().map(({ type, tooltip, isReady }) => {
return (
<Badge
key={type}
label={type}
tooltip={tooltip}
className={cssNames({ [type.toLowerCase()]: isReady })}
/>
)
})}
</DrawerItem>
{acme && (
<>
<DrawerTitle title="ACME"/>
{acme.config.map(({ domains, http01, dns01 }, index) => {
return (
<div key={index} className="acme-config">
<DrawerItem name={<Trans>Domains</Trans>} labelsOnly>
{domains.map(domain => <Badge key={domain} label={domain}/>)}
</DrawerItem>
<DrawerItem name={<Trans>Http01</Trans>}>
{Object.entries(http01).map(([key, val]) => `${key}: ${val}`)[0]}
</DrawerItem>
{dns01 && (
<DrawerItem name={<Trans>DNS Provider</Trans>} labelsOnly>
{dns01.provider}
</DrawerItem>
)}
</div>
)
})}
</>
)}
<KubeEventDetails object={cert}/>
</div>
);
}
}
apiManager.registerViews(certificatesApi, {
Details: CertificateDetails
})
@@ -1,26 +0,0 @@
@import "cert-manager.mixins";
.Certificates {
.TableCell {
&.name {
flex: 1.2;
}
&.type {
flex: .5;
}
&.status {
flex: .5;
@include table-cell-labels-offsets;
.Badge {
@include cert-status-bgc;
}
}
&.age {
flex: .5;
}
}
}
@@ -1,105 +0,0 @@
import "./certificates.scss"
import React from "react";
import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { KubeObjectMenu, KubeObjectMenuProps } from "../../kube-object/kube-object-menu";
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../../kube-object";
import { Certificate, certificatesApi } from "../../../api/endpoints/cert-manager.api";
import { cssNames, stopPropagation } from "../../../utils";
import { Link } from "react-router-dom";
import { Badge } from "../../badge";
import { apiManager } from "../../../api/api-manager";
import { Spinner } from "../../spinner";
enum sortBy {
name = "name",
namespace = "namespace",
age = "age",
commonName = "common-name",
secretName = "secret",
issuer = "issuer",
type = "type",
}
@observer
export class Certificates extends React.Component<KubeObjectListLayoutProps> {
render() {
const { store = apiManager.getStore(certificatesApi), ...layoutProps } = this.props;
if (!store) {
return <Spinner center/>
}
return (
<KubeObjectListLayout
{...layoutProps}
store={store}
className="Certificates"
sortingCallbacks={{
[sortBy.name]: (item: Certificate) => item.getName(),
[sortBy.namespace]: (item: Certificate) => item.getNs(),
[sortBy.secretName]: (item: Certificate) => item.getSecretName(),
[sortBy.commonName]: (item: Certificate) => item.getCommonName(),
[sortBy.issuer]: (item: Certificate) => item.getIssuerName(),
[sortBy.type]: (item: Certificate) => item.getType(),
}}
searchFilters={[
(item: Certificate) => item.getSearchFields(),
(item: Certificate) => item.getSecretName(),
(item: Certificate) => item.getCommonName(),
(item: Certificate) => item.getIssuerName(),
(item: Certificate) => item.getType(),
]}
renderHeaderTitle={<Trans>Certificates</Trans>}
renderTableHeader={[
{ title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name },
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
{ title: <Trans>Common Name</Trans>, className: "common-name", sortBy: sortBy.type },
{ title: <Trans>Type</Trans>, className: "type", sortBy: sortBy.type },
{ title: <Trans>Issuer</Trans>, className: "issuer", sortBy: sortBy.issuer },
{ title: <Trans>Secret</Trans>, className: "secret", sortBy: sortBy.secretName },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
{ title: <Trans>Status</Trans>, className: "status" },
]}
renderTableContents={(cert: Certificate) => {
return [
cert.getName(),
cert.getNs(),
cert.getCommonName(),
cert.getType(),
<Link to={cert.getIssuerDetailsUrl()} onClick={stopPropagation}>
{cert.getIssuerName()}
</Link>,
<Link to={cert.getSecretDetailsUrl()} onClick={stopPropagation}>
{cert.getSecretName()}
</Link>,
cert.getAge(),
cert.getConditions().map(({ type, tooltip, isReady }) => {
return (
<Badge
key={type}
label={type}
tooltip={tooltip}
className={cssNames({ [type.toLowerCase()]: isReady })}
/>
)
})
]
}}
renderItemMenu={(item: Certificate) => {
return <CertificateMenu object={item}/>
}}
/>
);
}
}
export function CertificateMenu(props: KubeObjectMenuProps<Certificate>) {
return (
<KubeObjectMenu {...props}/>
)
}
apiManager.registerViews(certificatesApi, {
List: Certificates,
Menu: CertificateMenu
})
@@ -1,4 +0,0 @@
export * from "./certificates"
export * from "./certificate-details"
export * from "./issuers"
export * from "./issuer-details"
@@ -1,7 +0,0 @@
@import "cert-manager.mixins";
.IssuerDetails {
.Badge {
@include cert-status-bgc;
}
}
@@ -1,175 +0,0 @@
import "./issuer-details.scss"
import React from "react";
import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { Link } from "react-router-dom";
import { DrawerItem, DrawerTitle } from "../../drawer";
import { Badge } from "../../badge";
import { KubeEventDetails } from "../../+events/kube-event-details";
import { KubeObjectDetailsProps } from "../../kube-object";
import { clusterIssuersApi, Issuer, issuersApi } from "../../../api/endpoints/cert-manager.api";
import { autobind, cssNames } from "../../../utils";
import { getDetailsUrl } from "../../../navigation";
import { secretsApi } from "../../../api/endpoints";
import { apiManager } from "../../../api/api-manager";
import { KubeObjectMeta } from "../../kube-object/kube-object-meta";
interface Props extends KubeObjectDetailsProps<Issuer> {
}
@observer
export class IssuerDetails extends React.Component<Props> {
@autobind()
renderSecretLink(secretName: string) {
const namespace = this.props.object.getNs();
if (!namespace) {
return secretName;
}
const secretDetailsUrl = getDetailsUrl(secretsApi.getUrl({
namespace: namespace,
name: secretName,
}));
return (
<Link to={secretDetailsUrl}>
{secretName}
</Link>
)
}
render() {
const { object: issuer, className } = this.props;
if (!issuer) return;
const { renderSecretLink } = this;
const { spec: { acme, ca, vault, venafi }, status } = issuer;
return (
<div className={cssNames("IssuerDetails", className)}>
<KubeObjectMeta object={issuer}/>
<DrawerItem name={<Trans>Type</Trans>}>
{issuer.getType()}
</DrawerItem>
<DrawerItem name={<Trans>Status</Trans>} labelsOnly>
{issuer.getConditions().map(({ type, tooltip, isReady }) => {
return (
<Badge
key={type}
label={type}
tooltip={tooltip}
className={cssNames({ [type.toLowerCase()]: isReady })}
/>
)
})}
</DrawerItem>
{acme && (() => {
const { email, server, skipTLSVerify, privateKeySecretRef, solvers } = acme;
return (
<>
<DrawerTitle title="ACME"/>
<DrawerItem name={<Trans>E-mail</Trans>}>
{email}
</DrawerItem>
<DrawerItem name={<Trans>Server</Trans>}>
{server}
</DrawerItem>
{status.acme && (
<DrawerItem name={<Trans>Status URI</Trans>}>
{status.acme.uri}
</DrawerItem>
)}
<DrawerItem name={<Trans>Private Key Secret</Trans>}>
{renderSecretLink(privateKeySecretRef.name)}
</DrawerItem>
<DrawerItem name={<Trans>Skip TLS Verify</Trans>}>
{skipTLSVerify ? <Trans>Yes</Trans> : <Trans>No</Trans>}
</DrawerItem>
</>
)
})()}
{ca && (() => {
const { secretName } = ca;
return (
<>
<DrawerTitle title="CA"/>
<DrawerItem name={<Trans>Secret Name</Trans>}>
{renderSecretLink(secretName)}
</DrawerItem>
</>
)
})()}
{vault && (() => {
const { auth, caBundle, path, server } = vault;
const { path: authPath, roleId, secretRef } = auth.appRole;
return (
<>
<DrawerTitle title="Vault"/>
<DrawerItem name={<Trans>Server</Trans>}>
{server}
</DrawerItem>
<DrawerItem name={<Trans>Path</Trans>}>
{path}
</DrawerItem>
<DrawerItem name={<Trans>CA Bundle</Trans>} labelsOnly>
<Badge label={caBundle}/>
</DrawerItem>
<DrawerTitle title={<Trans>Auth App Role</Trans>}/>
<DrawerItem name={<Trans>Path</Trans>}>
{authPath}
</DrawerItem>
<DrawerItem name={<Trans>Role ID</Trans>}>
{roleId}
</DrawerItem>
{secretRef && (
<DrawerItem name={<Trans>Secret</Trans>}>
{renderSecretLink(secretRef.name)}
</DrawerItem>
)}
</>
)
})()}
{venafi && (() => {
const { zone, cloud, tpp } = venafi;
return (
<>
<DrawerTitle title="CA"/>
<DrawerItem name={<Trans>Zone</Trans>}>
{zone}
</DrawerItem>
{cloud && (
<DrawerItem name={<Trans>Cloud API Token Secret</Trans>}>
{renderSecretLink(cloud.apiTokenSecretRef.name)}
</DrawerItem>
)}
{tpp && (
<>
<DrawerTitle title="TPP"/>
<DrawerItem name={<Trans>URL</Trans>}>
{tpp.url}
</DrawerItem>
<DrawerItem name={<Trans>CA Bundle</Trans>} labelsOnly>
<Badge label={tpp.caBundle}/>
</DrawerItem>
<DrawerItem name={<Trans>Credentials Ref</Trans>}>
{renderSecretLink(tpp.credentialsRef.name)}
</DrawerItem>
</>
)}
</>
)
})()}
<KubeEventDetails object={issuer}/>
</div>
);
}
}
apiManager.registerViews([issuersApi, clusterIssuersApi], {
Details: IssuerDetails
})
@@ -1,27 +0,0 @@
@import "cert-manager.mixins";
.Issuers {
.TableCell {
&.name {
flex: 1.2;
}
&.labels {
flex: 2;
@include table-cell-labels-offsets;
}
&.status {
flex: .5;
@include table-cell-labels-offsets;
.Badge {
@include cert-status-bgc;
}
}
&.age {
flex: .5;
}
}
}
@@ -1,103 +0,0 @@
import "./issuers.scss"
import React from "react";
import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { KubeObjectMenu, KubeObjectMenuProps } from "../../kube-object/kube-object-menu";
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../../kube-object";
import { clusterIssuersApi, Issuer, issuersApi } from "../../../api/endpoints/cert-manager.api";
import { cssNames } from "../../../utils";
import { Badge } from "../../badge";
import { Spinner } from "../../spinner";
import { apiManager } from "../../../api/api-manager";
enum sortBy {
name = "name",
namespace = "namespace",
type = "type",
labels = "labels",
age = "age",
}
@observer
export class ClusterIssuers extends React.Component<KubeObjectListLayoutProps> {
render() {
const store = apiManager.getStore(clusterIssuersApi);
return (
<Issuers
{...this.props}
isClusterScoped={true}
store={store}
renderHeaderTitle={<Trans>Cluster Issuers</Trans>}
/>
)
}
}
@observer
export class Issuers extends React.Component<KubeObjectListLayoutProps> {
render() {
const { store = apiManager.getStore(issuersApi), ...layoutProps } = this.props;
if (!store) {
return <Spinner center/>
}
return (
<KubeObjectListLayout
store={store}
renderHeaderTitle={<Trans>Issuers</Trans>}
{...layoutProps}
className="Issuers"
sortingCallbacks={{
[sortBy.name]: (item: Issuer) => item.getName(),
[sortBy.namespace]: (item: Issuer) => item.getNs(),
[sortBy.type]: (item: Issuer) => item.getType(),
[sortBy.labels]: (item: Issuer) => item.getLabels(),
[sortBy.age]: (item: Issuer) => item.metadata.creationTimestamp,
}}
searchFilters={[
(item: Issuer) => item.getSearchFields(),
(item: Issuer) => item.getType(),
]}
renderTableHeader={[
{ title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name },
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
{ title: <Trans>Labels</Trans>, className: "labels", sortBy: sortBy.labels },
{ title: <Trans>Type</Trans>, className: "type", sortBy: sortBy.type },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
{ title: <Trans>Status</Trans>, className: "status" },
]}
renderTableContents={(issuer: Issuer) => [
issuer.getName(),
issuer.getNs(),
issuer.getLabels().map(label => <Badge key={label} label={label} title={label}/>),
issuer.getType(),
issuer.getAge(),
issuer.getConditions().map(({ type, tooltip, isReady }) => {
return (
<Badge
key={type}
label={type}
tooltip={tooltip}
className={cssNames({ [type.toLowerCase()]: isReady })}
/>
)
})
]}
renderItemMenu={(item: Issuer) => {
return <IssuerMenu object={item}/>
}}
/>
);
}
}
export function IssuerMenu(props: KubeObjectMenuProps<Issuer>) {
return (
<KubeObjectMenu {...props}/>
)
}
apiManager.registerViews([issuersApi, clusterIssuersApi], {
List: Issuers,
Menu: IssuerMenu,
})
@@ -14,6 +14,7 @@ import { KubeObjectDetailsProps } from "../kube-object";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { Input } from "../input";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
}
@@ -133,6 +134,10 @@ export class CRDDetails extends React.Component<Props> {
}
}
apiManager.registerViews(crdApi, {
Details: CRDDetails
})
kubeObjectDetailRegistry.add({
kind: "CustomResourceDefinition",
apiVersions: ["apiextensions.k8s.io/v1", "apiextensions.k8s.io/v1beta1"],
components: {
Details: (props) => <CRDDetails {...props} />
}
})
@@ -8,9 +8,7 @@ import { Link } from "react-router-dom";
import { stopPropagation } from "../../utils";
import { KubeObjectListLayout } from "../kube-object";
import { crdStore } from "./crd.store";
import { apiManager } from "../../api/api-manager";
import { crdApi, CustomResourceDefinition } from "../../api/endpoints/crd.api";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { CustomResourceDefinition } from "../../api/endpoints/crd.api";
import { Select, SelectOption } from "../select";
import { navigation, setQueryParams } from "../../navigation";
import { Icon } from "../icon";
@@ -103,20 +101,8 @@ export class CrdList extends React.Component {
crd.getAge(),
]
}}
renderItemMenu={(item: CustomResourceDefinition) => {
return <CRDMenu object={item}/>
}}
/>
)
}
}
export function CRDMenu(props: KubeObjectMenuProps<CustomResourceDefinition>) {
return (
<KubeObjectMenu {...props}/>
)
}
apiManager.registerViews(crdApi, {
Menu: CRDMenu,
});
@@ -9,7 +9,6 @@ import { cssNames } from "../../utils";
import { Badge } from "../badge";
import { DrawerItem } from "../drawer";
import { KubeObjectDetailsProps } from "../kube-object";
import { apiManager } from "../../api/api-manager";
import { crdStore } from "./crd.store";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Input } from "../input";
@@ -39,20 +38,13 @@ export class CrdResourceDetails extends React.Component<Props> {
return crdStore.getByObject(this.props.object);
}
@computed get CustomDetailsViews() {
return apiManager.getViews(this.props.object.selfLink).Details;
}
render() {
const { object } = this.props;
const { crd, CustomDetailsViews } = this;
const { crd } = this;
if (!object || !crd) return null;
const className = cssNames("CrdResourceDetails", crd.getResourceKind());
const extraColumns = crd.getPrinterColumns();
const showStatus = !extraColumns.find(column => column.name == "Status") && object.status?.conditions;
if (CustomDetailsViews) {
return <CustomDetailsViews className={className} object={object}/>
}
return (
<div className={className}>
<KubeObjectMeta object={object}/>
@@ -7,7 +7,6 @@ import { Trans } from "@lingui/macro";
import { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object";
import { KubeObject } from "../../api/kube-object";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { ICRDRouteParams } from "./crd.route";
import { autorun, computed } from "mobx";
import { crdStore } from "./crd.store";
@@ -59,9 +58,7 @@ export class CrdResources extends React.Component<Props> {
extraColumns.forEach(column => {
sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.query(item, column.jsonPath.slice(1))
})
// todo: merge extra columns and other params to predefined view
const { List } = apiManager.getViews(crd.getResourceApiBase());
const ListView = List || KubeObjectListLayout;
const ListView = KubeObjectListLayout;
return (
<ListView
className="CrdResources"
@@ -93,20 +90,7 @@ export class CrdResources extends React.Component<Props> {
}),
crdInstance.getAge(),
]}
renderItemMenu={(item: KubeObject) => {
return <CrdResourceMenu object={item}/>
}}
/>
)
}
}
export function CrdResourceMenu(props: KubeObjectMenuProps<KubeObject>) {
const { Menu } = apiManager.getViews(props.object.selfLink);
if (Menu) {
return <Menu {...props}/>
}
return (
<KubeObjectMenu {...props}/>
)
}
@@ -3,6 +3,3 @@ export * from "./crd-list";
export * from "./crd-details";
export * from "./crd-resources";
export * from "./crd-resource-details";
// customized crd-s
export * from "./certmanager.k8s.io"