Skip to content

Commit

Permalink
Add KustomizationDetail page
Browse files Browse the repository at this point in the history
  • Loading branch information
jpellizzari committed Feb 22, 2022
1 parent 0f43ebc commit 32a1f0c
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 15 deletions.
11 changes: 9 additions & 2 deletions core/server/fluxruntime.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
)

var (
Expand Down Expand Up @@ -98,8 +99,14 @@ func (cs *coreServer) GetReconciledObjects(ctx context.Context, msg *pb.GetRecon
})

if err := k8s.List(ctx, &l, opts, client.InNamespace(msg.Namespace)); err != nil {
// return nil, fmt.Errorf("could not get unstructured list: %s", err)
continue
if k8serrors.IsForbidden(err) {
// Our service account (or impersonated user) may not have the ability to see the resource in question,
// in the given namespace.
// We pretend it doesn't exist and keep looping.
continue
}

return nil, fmt.Errorf("listing unstructured object: %w", err)
}

result = append(result, l.Items...)
Expand Down
15 changes: 15 additions & 0 deletions ui/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MuiThemeProvider } from "@material-ui/core";
import qs from "query-string";
import * as React from "react";
import { QueryClient, QueryClientProvider } from "react-query";
import {
Expand All @@ -20,10 +21,19 @@ import { V2Routes } from "./lib/types";
import Error from "./pages/Error";
import Automations from "./pages/v2/Automations";
import FluxRuntime from "./pages/v2/FluxRuntime";
import KustomizationDetail from "./pages/v2/KustomizationDetail";
import Sources from "./pages/v2/Sources";

const queryClient = new QueryClient();

function withName(Cmp) {
return ({ location: { search }, ...rest }) => {
const params = qs.parse(search);

return <Cmp {...rest} name={params.name as string} />;
};
}

export default function App() {
return (
<MuiThemeProvider theme={muiTheme}>
Expand All @@ -41,6 +51,11 @@ export default function App() {
path={V2Routes.Automations}
component={Automations}
/>
<Route
exact
path={V2Routes.Kustomization}
component={withName(KustomizationDetail)}
/>
<Route exact path={V2Routes.Sources} component={Sources} />
<Route
exact
Expand Down
1 change: 0 additions & 1 deletion ui/components/AutomationsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ type Props = {
};

function AutomationsTable({ className, automations }: Props) {
console.log(automations);
return (
<DataTable
className={className}
Expand Down
69 changes: 69 additions & 0 deletions ui/components/ReconciledObjectsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import _ from "lodash";
import * as React from "react";
import styled from "styled-components";
import { useGetReconciledObjects } from "../hooks/flux";
import {
AutomationKind,
GroupVersionKind,
UnstructuredObject,
} from "../lib/api/core/types.pb";
import DataTable from "./DataTable";
import KubeStatusIndicator from "./KubeStatusIndicator";

type Props = {
className?: string;
automationName: string;
namespace: string;
automationKind: AutomationKind;
kinds: GroupVersionKind[];
};

function ReconciledObjectsTable({
className,
automationName,
namespace,
automationKind,
kinds,
}: Props) {
const objs = useGetReconciledObjects(
automationName,
namespace,
automationKind,
kinds
);

return (
<div className={className}>
<DataTable
sortFields={["name", "type", "namespace", "status"]}
fields={[
{ value: "name", label: "Name" },
{
label: "Type",
value: (u: UnstructuredObject) => `${u.groupVersionKind.kind}`,
},
{
label: "Namespace",
value: "namespace",
},
{
label: "Status",
value: (u: UnstructuredObject) =>
u.conditions.length > 0 ? (
<KubeStatusIndicator conditions={u.conditions} />
) : null,
},
{
label: "Message",
value: (u: UnstructuredObject) => _.first(u.conditions)?.message,
},
]}
rows={objs}
/>
</div>
);
}

export default styled(ReconciledObjectsTable).attrs({
className: ReconciledObjectsTable.name,
})``;
11 changes: 11 additions & 0 deletions ui/hooks/automations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import _ from "lodash";
import { useContext } from "react";
import { useQuery } from "react-query";
import { AppContext } from "../contexts/AppContext";
import { GetKustomizationResponse } from "../lib/api/core/core.pb";
import { Kustomization } from "../lib/api/core/types.pb";
import { AutomationType, RequestError, WeGONamespace } from "../lib/types";

Expand Down Expand Up @@ -31,3 +32,13 @@ export function useListAutomations(namespace = WeGONamespace) {
{ retry: false }
);
}

export function useGetKustomization(name: string, namespace = WeGONamespace) {
const { api } = useContext(AppContext);

return useQuery<GetKustomizationResponse, RequestError>(
["kustomizations", name],
() => api.GetKustomization({ name, namespace }),
{ retry: false }
);
}
24 changes: 23 additions & 1 deletion ui/hooks/flux.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useContext } from "react";
import { useContext, useEffect, useState } from "react";
import { useQuery } from "react-query";
import { AppContext } from "../contexts/AppContext";
import { ListFluxRuntimeObjectsResponse } from "../lib/api/core/core.pb";
import { AutomationKind, GroupVersionKind } from "../lib/api/core/types.pb";
import { getChildren } from "../lib/graph";
import { RequestError, WeGONamespace } from "../lib/types";

export function useListFluxRuntimeObjects(namespace = WeGONamespace) {
Expand All @@ -13,3 +15,23 @@ export function useListFluxRuntimeObjects(namespace = WeGONamespace) {
{ retry: false }
);
}

export function useGetReconciledObjects(
name: string,
namespace: string,
type: AutomationKind,
kinds: GroupVersionKind[]
) {
const { api } = useContext(AppContext);
const [res, setRes] = useState([]);

useEffect(() => {
if (!name || !namespace || !type || kinds.length === 0) {
return;
}

getChildren(api, name, namespace, kinds).then((res) => setRes(res));
}, [type, kinds]);

return res;
}
31 changes: 20 additions & 11 deletions ui/lib/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
// with in the context of their parent-child relationships.
import _ from "lodash";
import {
Application,
Applications,
GroupVersionKind,
UnstructuredObject,
} from "./api/applications/applications.pb";
import { Core } from "./api/core/core.pb";

export type UnstructuredObjectWithParent = UnstructuredObject & {
parentUid?: string;
Expand All @@ -32,7 +31,8 @@ export const PARENT_CHILD_LOOKUP = {
};

export const getChildrenRecursive = async (
appsClient: typeof Applications,
client: typeof Core,
namespace: string,
result: UnstructuredObjectWithParent[],
object: UnstructuredObjectWithParent,
lookup: any
Expand All @@ -45,8 +45,9 @@ export const getChildrenRecursive = async (
for (let i = 0; i < k.children.length; i++) {
const child: GroupVersionKind = k.children[i];

const res = await appsClient.GetChildObjects({
const res = await client.GetChildObjects({
parentUid: object.uid,
namespace,
groupVersionKind: child,
});

Expand All @@ -55,7 +56,8 @@ export const getChildrenRecursive = async (

// Dive down one level and update the lookup accordingly.
await getChildrenRecursive(
appsClient,
client,
namespace,
result,
{ ...c, parentUid: object.uid },
{
Expand All @@ -69,21 +71,28 @@ export const getChildrenRecursive = async (

// Gets the "child" objects that result from an Application
export const getChildren = async (
appsClient: typeof Applications,
app: Application,
client: typeof Core,
automationName,
namespace,
kinds: GroupVersionKind[]
): Promise<UnstructuredObject[]> => {
const { objects } = await appsClient.GetReconciledObjects({
automationName: app.name,
automationNamespace: app.namespace,
const { objects } = await client.GetReconciledObjects({
automationName,
namespace,
kinds,
});

const result = [];
for (let o = 0; o < objects.length; o++) {
const obj = objects[o];

await getChildrenRecursive(appsClient, result, obj, PARENT_CHILD_LOOKUP);
await getChildrenRecursive(
client,
namespace,
result,
obj,
PARENT_CHILD_LOOKUP
);
}

return _.flatten(result);
Expand Down
114 changes: 114 additions & 0 deletions ui/pages/v2/KustomizationDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import _ from "lodash";
import * as React from "react";
import styled from "styled-components";
import Flex from "../../components/Flex";
import KubeStatusIndicator from "../../components/KubeStatusIndicator";
import Link from "../../components/Link";
import Page from "../../components/Page";
import ReconciledObjectsTable from "../../components/ReconciledObjectsTable";
import Text from "../../components/Text";
import { useGetKustomization } from "../../hooks/automations";
import { AutomationKind } from "../../lib/api/core/types.pb";
import { formatURL } from "../../lib/nav";
import { V2Routes, WeGONamespace } from "../../lib/types";

type Props = {
name: string;
className?: string;
};

const Info = styled.div`
padding-bottom: 32px;
`;

const InfoList = styled(
({
items,
className,
}: {
className?: string;
items: { [key: string]: any };
}) => {
return (
<table className={className}>
<tbody>
{_.map(items, (v, k) => (
<tr key={k}>
<td>
<Text capitalize bold>
{k}:
</Text>
</td>
<td>{v}</td>
</tr>
))}
</tbody>
</table>
);
}
)`
tbody tr td:first-child {
min-width: 100px;
}
tr {
height: 16px;
}
`;

function KustomizationDetail({ className, name }: Props) {
const { data, isLoading, error } = useGetKustomization(name);

const kustomization = data?.kustomization;

return (
<Page
title={kustomization?.name}
loading={isLoading}
error={error}
className={className}
>
<Info>
<h3>{kustomization?.namespace}</h3>
<InfoList
items={{
Source: (
<Link
to={formatURL(V2Routes.GitRepo, {
name: kustomization?.sourceRef.name,
})}
>
GitRepository/{kustomization?.sourceRef.name}
</Link>
),
Status: (
<Flex start>
<KubeStatusIndicator conditions={kustomization?.conditions} />
<div>
&nbsp; Applied revision {kustomization?.lastAppliedRevision}
</div>
</Flex>
),
Cluster: "",
Path: kustomization?.path,
}}
/>
</Info>
<ReconciledObjectsTable
kinds={kustomization?.inventory}
automationName={kustomization?.name}
namespace={WeGONamespace}
automationKind={AutomationKind.KustomizationAutomation}
/>
</Page>
);
}

export default styled(KustomizationDetail).attrs({
className: KustomizationDetail.name,
})`
h3 {
color: #737373;
font-weight: 200;
margin-top: 12px;
}
`;

0 comments on commit 32a1f0c

Please sign in to comment.