Skip to content

Commit

Permalink
OperatorInstance view (#1594)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andres Martinez Gotor authored Mar 23, 2020
1 parent 3a2acab commit de6afe9
Show file tree
Hide file tree
Showing 19 changed files with 799 additions and 60 deletions.
155 changes: 155 additions & 0 deletions dashboard/src/actions/operators.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,158 @@ describe("getResources", () => {
expect(store.getActions()).toEqual(expectedActions);
});
});

describe("getResources", () => {
it("get a resource in a namespace", async () => {
const csv = {
metadata: { name: "foo" },
spec: {
customresourcedefinitions: { owned: [{ name: "foo.kubeapps.com", version: "v1alpha1" }] },
},
};
const resource = { metadata: { name: "resource" } };
Operators.getCSV = jest.fn(() => csv);
Operators.getResource = jest.fn(() => resource);
const expectedActions = [
{
type: getType(operatorActions.requestCustomResource),
},
{
type: getType(operatorActions.requestCSV),
},
{
type: getType(operatorActions.receiveCSV),
payload: csv,
},
{
type: getType(operatorActions.receiveCustomResource),
payload: resource,
},
];
await store.dispatch(operatorActions.getResource("default", "foo", "foo.kubeapps.com", "bar"));
expect(store.getActions()).toEqual(expectedActions);
expect(Operators.getResource).toHaveBeenCalledWith(
"default",
"kubeapps.com/v1alpha1",
"foo",
"bar",
);
});

it("dispatches an error if getting a resource fails", async () => {
const csv = {
metadata: { name: "foo" },
spec: {
customresourcedefinitions: { owned: [{ name: "foo.kubeapps.com", version: "v1alpha1" }] },
},
};
Operators.getCSV = jest.fn(() => csv);
Operators.getResource = jest.fn(() => {
throw new Error("Boom!");
});
const expectedActions = [
{
type: getType(operatorActions.requestCustomResource),
},
{
type: getType(operatorActions.requestCSV),
},
{
type: getType(operatorActions.receiveCSV),
payload: csv,
},
{
type: getType(operatorActions.errorCustomResource),
payload: new Error("Boom!"),
},
];
await store.dispatch(operatorActions.getResource("default", "foo", "foo.kubeapps.com", "bar"));
expect(store.getActions()).toEqual(expectedActions);
});

it("dispatches an error if the given csv is not found", async () => {
Operators.getCSV = jest.fn(() => undefined);
const expectedActions = [
{
type: getType(operatorActions.requestCustomResource),
},
{
type: getType(operatorActions.requestCSV),
},
{
type: getType(operatorActions.receiveCSV),
},
{
type: getType(operatorActions.errorCustomResource),
payload: new Error("CSV foo not found in default"),
},
];
await store.dispatch(operatorActions.getResource("default", "foo", "foo.kubeapps.com", "bar"));
expect(store.getActions()).toEqual(expectedActions);
});

it("dispatches an error if the given crd is not found fails", async () => {
const csv = {
metadata: { name: "foo" },
spec: {
customresourcedefinitions: { owned: [{ name: "foo.kubeapps.com", version: "v1alpha1" }] },
},
};
Operators.getCSV = jest.fn(() => csv);
const expectedActions = [
{
type: getType(operatorActions.requestCustomResource),
},
{
type: getType(operatorActions.requestCSV),
},
{
type: getType(operatorActions.receiveCSV),
payload: csv,
},
{
type: getType(operatorActions.errorCustomResource),
payload: new Error("Not found a valid CRD definition for foo/not-foo.kubeapps.com"),
},
];
await store.dispatch(
operatorActions.getResource("default", "foo", "not-foo.kubeapps.com", "bar"),
);
expect(store.getActions()).toEqual(expectedActions);
});
});

describe("deleteResource", () => {
it("delete a resource in a namespace", async () => {
const resource = { metadata: { name: "resource" } } as any;
Operators.deleteResource = jest.fn();
const expectedActions = [
{
type: getType(operatorActions.deletingResource),
},
{
type: getType(operatorActions.resourceDeleted),
},
];
await store.dispatch(operatorActions.deleteResource("default", "foos", resource));
expect(store.getActions()).toEqual(expectedActions);
});

it("dispatches an error if deleting a resource fails", async () => {
const resource = { metadata: { name: "resource" } } as any;
Operators.deleteResource = jest.fn(() => {
throw new Error("Boom!");
});
const expectedActions = [
{
type: getType(operatorActions.deletingResource),
},
{
type: getType(operatorActions.errorResourceDelete),
payload: new Error("Boom!"),
},
];
await store.dispatch(operatorActions.deleteResource("default", "foos", resource));
expect(store.getActions()).toEqual(expectedActions);
});
});
99 changes: 91 additions & 8 deletions dashboard/src/actions/operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export const errorResourceCreate = createAction("ERROR_RESOURCE_CREATE", resolve
return (err: Error) => resolve(err);
});

export const deletingResource = createAction("DELETING_RESOURCE");
export const resourceDeleted = createAction("RESOURCE_DELETED");
export const errorResourceDelete = createAction("ERROR_RESOURCE_DELETE", resolve => {
return (err: Error) => resolve(err);
});

export const requestCustomResources = createAction("REQUEST_CUSTOM_RESOURCES");
export const receiveCustomResources = createAction("RECEIVE_CUSTOM_RESOURCES", resolve => {
return (resources: IResource[]) => resolve(resources);
Expand All @@ -54,6 +60,11 @@ export const errorCustomResource = createAction("ERROR_CUSTOM_RESOURCE", resolve
return (err: Error) => resolve(err);
});

export const requestCustomResource = createAction("REQUEST_CUSTOM_RESOURCE");
export const receiveCustomResource = createAction("RECEIVE_CUSTOM_RESOURCE", resolve => {
return (resource: IResource) => resolve(resource);
});

const actions = [
checkingOLM,
OLMInstalled,
Expand All @@ -74,6 +85,11 @@ const actions = [
requestCustomResources,
receiveCustomResources,
errorCustomResource,
requestCustomResource,
receiveCustomResource,
deletingResource,
resourceDeleted,
errorResourceDelete,
];

export type OperatorAction = ActionType<typeof actions[number]>;
Expand Down Expand Up @@ -142,14 +158,16 @@ export function getCSVs(
export function getCSV(
namespace: string,
name: string,
): ThunkAction<Promise<void>, IStoreState, null, OperatorAction> {
): ThunkAction<Promise<IClusterServiceVersion | undefined>, IStoreState, null, OperatorAction> {
return async dispatch => {
dispatch(requestCSV());
try {
const csv = await Operators.getCSV(namespace, name);
dispatch(receiveCSV(csv));
return csv;
} catch (e) {
dispatch(errorCSVs(e));
return;
}
};
}
Expand All @@ -173,24 +191,51 @@ export function createResource(
};
}

export function deleteResource(
namespace: string,
plural: string,
resource: IResource,
): ThunkAction<Promise<boolean>, IStoreState, null, OperatorAction> {
return async dispatch => {
dispatch(deletingResource());
try {
await Operators.deleteResource(
namespace,
resource.apiVersion,
plural,
resource.metadata.name,
);
dispatch(resourceDeleted());
return true;
} catch (e) {
dispatch(errorResourceDelete(e));
return false;
}
};
}

function parseCRD(crdName: string) {
const parsedCRD = crdName.split(".");
const plural = parsedCRD[0];
const group = parsedCRD.slice(1).join(".");
return { plural, group };
}

export function getResources(
namespace: string,
): ThunkAction<Promise<void>, IStoreState, null, OperatorAction> {
): ThunkAction<Promise<IResource[]>, IStoreState, null, OperatorAction> {
return async dispatch => {
dispatch(requestCustomResources());
const csvs = await dispatch(getCSVs(namespace));
let resources: IResource[] = [];
const csvPromises = csvs.map(async csv => {
const crdPromises = csv.spec.customresourcedefinitions.owned.map(async crd => {
const parsedCRD = crd.name.split(".");
const name = parsedCRD[0];
const group = parsedCRD.slice(1).join(".");
const groupVersion = crd.version;
const { plural, group } = parseCRD(crd.name);
try {
const csvResources = await Operators.listResources(
namespace,
`${group}/${groupVersion}`,
name,
`${group}/${crd.version}`,
plural,
);
resources = resources.concat(csvResources.items);
} catch (e) {
Expand All @@ -203,5 +248,43 @@ export function getResources(
if (resources.length) {
dispatch(receiveCustomResources(resources));
}
return resources;
};
}

export function getResource(
namespace: string,
csvName: string,
crdName: string,
resourceName: string,
): ThunkAction<Promise<void>, IStoreState, null, OperatorAction> {
return async dispatch => {
dispatch(requestCustomResource());
const csv = await dispatch(getCSV(namespace, csvName));
if (csv) {
const crd = csv.spec.customresourcedefinitions.owned.find(c => c.name === crdName);
if (crd) {
const { plural, group } = parseCRD(crd.name);
try {
const resource = await Operators.getResource(
namespace,
`${group}/${crd.version}`,
plural,
resourceName,
);
dispatch(receiveCustomResource(resource));
} catch (e) {
dispatch(errorCustomResource(e));
}
} else {
dispatch(
errorCustomResource(
new Error(`Not found a valid CRD definition for ${csvName}/${crdName}`),
),
);
}
} else {
dispatch(errorCustomResource(new Error(`CSV ${csvName} not found in ${namespace}`)));
}
};
}
11 changes: 3 additions & 8 deletions dashboard/src/components/AppList/AppList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,10 @@ class AppList extends React.Component<IAppListProps, IAppListState> {
return <AppListItem key={r.releaseName} app={r} />;
})}
{filteredCRs.map(r => {
return (
<CustomResourceListItem
key={r.metadata.name}
resource={r}
csv={this.props.csvs.find(csv =>
csv.spec.customresourcedefinitions.owned.some(crd => crd.kind === r.kind),
)}
/>
const csv = this.props.csvs.find(c =>
c.spec.customresourcedefinitions.owned.some(crd => crd.kind === r.kind),
);
return <CustomResourceListItem key={r.metadata.name} resource={r} csv={csv!} />;
})}
</CardGrid>
</div>
Expand Down
18 changes: 14 additions & 4 deletions dashboard/src/components/AppList/CustomResourceListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,36 @@ import * as React from "react";

import placeholder from "../../placeholder.png";
import { IClusterServiceVersion, IResource } from "../../shared/types";
import UnexpectedErrorPage from "../ErrorAlert/UnexpectedErrorAlert";
import InfoCard from "../InfoCard";

interface ICustomResourceListItemProps {
resource: IResource;
csv?: IClusterServiceVersion;
csv: IClusterServiceVersion;
}

class CustomResourceListItem extends React.Component<ICustomResourceListItemProps> {
public render() {
const { resource, csv } = this.props;
const icon = csv?.spec.icon
const icon = csv.spec.icon
? `data:${csv.spec.icon[0].mediatype};base64,${csv.spec.icon[0].base64data}`
: placeholder;
const crd = csv.spec.customresourcedefinitions.owned.find(c => c.kind === resource.kind);
if (!crd) {
// Unexpected error, CRD should be present if resource is defined
return (
<UnexpectedErrorPage
text={`Unable to retrieve the CustomResourceDefinition for ${resource.kind} in ${csv.metadata.name}`}
/>
);
}
return (
<InfoCard
key={resource.metadata.name}
link={`/operators-instances/ns/${resource.metadata.namespace}/${resource.metadata.name}`}
link={`/operators-instances/ns/${resource.metadata.namespace}/${csv.metadata.name}/${crd.name}/${resource.metadata.name}`}
title={resource.metadata.name}
icon={icon}
info={`${resource.kind} v${csv?.spec.version || "-"}`}
info={`${resource.kind} v${csv.spec.version || "-"}`}
tag1Content={resource.metadata.namespace}
tag2Content="operator"
/>
Expand Down
4 changes: 4 additions & 0 deletions dashboard/src/components/AppView/AppNotes.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.Terminal {
overflow-wrap: break-word;
}

.Terminal__Code code {
white-space: pre-wrap;
}
Loading

0 comments on commit de6afe9

Please sign in to comment.