From f66d84ae4a41d1432f509163f479497b1ff2f564 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 15 Dec 2020 13:29:30 -0500 Subject: [PATCH] add more typing to sorting of tables Signed-off-by: Sebastian Malton --- .../kube-object-event-status/src/resolver.tsx | 2 +- src/extensions/extension-discovery.ts | 9 ++- src/renderer/api/api-manager.ts | 6 +- .../api/endpoints/resource-applier.api.ts | 20 +++--- .../+custom-resources/crd-resource.store.ts | 7 +- src/renderer/components/+events/events.tsx | 22 +++---- .../service-accounts-details.tsx | 23 ++++--- .../+workloads-overview/overview.tsx | 64 ++++++------------- .../components/+workloads/workloads.stores.ts | 2 +- .../components/dock/create-resource.tsx | 10 +-- .../item-object-list/item-list-layout.tsx | 30 ++++----- .../kube-object/kube-object-details.tsx | 3 +- .../kube-object/kube-object-list-layout.tsx | 4 +- src/renderer/components/table/table-cell.tsx | 10 +-- src/renderer/components/table/table.tsx | 9 ++- src/renderer/kube-object.store.ts | 25 ++++---- src/renderer/utils/cancelableFetch.ts | 2 +- 17 files changed, 110 insertions(+), 138 deletions(-) diff --git a/extensions/kube-object-event-status/src/resolver.tsx b/extensions/kube-object-event-status/src/resolver.tsx index 69691c2e79..5e9151288f 100644 --- a/extensions/kube-object-event-status/src/resolver.tsx +++ b/extensions/kube-object-event-status/src/resolver.tsx @@ -56,4 +56,4 @@ export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeOb text: `${event.message}`, timestamp: event.metadata.creationTimestamp }; -} \ No newline at end of file +} diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 73bddd7481..c278e456fc 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -269,7 +269,6 @@ export class ExtensionDiscovery { // fs.remove won't throw if path is missing await fs.remove(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json")); - try { // Verify write access to static/extensions, which is needed for symlinking await fs.access(this.inTreeFolderPath, fs.constants.W_OK); @@ -293,19 +292,19 @@ export class ExtensionDiscovery { this.bundledFolderPath = this.inTreeTargetPath; } - await fs.ensureDir(this.nodeModulesPath); - await fs.ensureDir(this.localFolderPath); + + await Promise.all([fs.ensureDir(this.nodeModulesPath), fs.ensureDir(this.localFolderPath)]); const extensions = await this.loadExtensions(); this.isLoaded = true; - + return extensions; } /** * Returns the symlinked path to the extension folder, - * e.g. "/Users//Library/Application Support/Lens/node_modules/@publisher/extension" + * e.g. `/Users//Library/Application Support/Lens/node_modules/@publisher/extension` */ protected getInstalledPath(name: string) { return path.join(this.nodeModulesPath, name); diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts index 12ad8910c2..4280b2c655 100644 --- a/src/renderer/api/api-manager.ts +++ b/src/renderer/api/api-manager.ts @@ -10,12 +10,12 @@ export class ApiManager { private apis = observable.map(); private stores = observable.map>(); - getApi(pathOrCallback: string | ((api: KubeApi) => boolean) = () => true) { + getApi(pathOrCallback: string | ((api: KubeApi) => boolean) = () => true): KubeApi { if (typeof pathOrCallback === "string") { - return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase); + return (this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase)) as KubeApi; } - return Array.from(this.apis.values()).find(pathOrCallback); + return Array.from(this.apis.values()).find(pathOrCallback) as KubeApi; } registerApi(apiBase: string, api: KubeApi) { diff --git a/src/renderer/api/endpoints/resource-applier.api.ts b/src/renderer/api/endpoints/resource-applier.api.ts index a397cfdda0..397716aa93 100644 --- a/src/renderer/api/endpoints/resource-applier.api.ts +++ b/src/renderer/api/endpoints/resource-applier.api.ts @@ -3,31 +3,29 @@ import { KubeObject } from "../kube-object"; import { KubeJsonApiData } from "../kube-json-api"; import { apiBase } from "../index"; import { apiManager } from "../api-manager"; +import { CancelablePromise } from "../../utils/cancelableFetch"; +import { filter } from "lodash"; export const resourceApplierApi = { annotations: [ "kubectl.kubernetes.io/last-applied-configuration" ], - async update(resource: object | string): Promise { + update(resource: object | string): CancelablePromise { if (typeof resource === "string") { resource = jsYaml.safeLoad(resource); } return apiBase .post("/stack", { data: resource }) - .then(data => { - const items = data.map(obj => { - const api = apiManager.getApi(obj.metadata.selfLink); + .then(data => filter( + data.map(obj => { + const api = apiManager.getApi(obj.metadata.selfLink); - if (api) { + if (api?.objectConstructor) { return new api.objectConstructor(obj); - } else { - return new KubeObject(obj); } - }); - - return items.length === 1 ? items[0] : items; - }); + }) + )); } }; diff --git a/src/renderer/components/+custom-resources/crd-resource.store.ts b/src/renderer/components/+custom-resources/crd-resource.store.ts index 963ca3cd17..f3f45a37ef 100644 --- a/src/renderer/components/+custom-resources/crd-resource.store.ts +++ b/src/renderer/components/+custom-resources/crd-resource.store.ts @@ -4,11 +4,8 @@ import { KubeObjectStore } from "../../kube-object.store"; import { KubeObject } from "../../api/kube-object"; @autobind() -export class CRDResourceStore extends KubeObjectStore { - api: KubeApi; - - constructor(api: KubeApi) { +export class CRDResourceStore extends KubeObjectStore { + constructor(public api: KubeApi) { super(); - this.api = api; } } diff --git a/src/renderer/components/+events/events.tsx b/src/renderer/components/+events/events.tsx index 3d6977c656..91e1000b17 100644 --- a/src/renderer/components/+events/events.tsx +++ b/src/renderer/components/+events/events.tsx @@ -22,7 +22,7 @@ enum sortBy { age = "age", } -interface Props extends Partial { +interface Props extends Partial> { className?: IClassName; compact?: boolean; compactLimit?: number; @@ -45,17 +45,17 @@ export class Events extends React.Component { store={eventStore} isSelectable={false} sortingCallbacks={{ - [sortBy.namespace]: (event: KubeEvent) => event.getNs(), - [sortBy.type]: (event: KubeEvent) => event.involvedObject.kind, - [sortBy.object]: (event: KubeEvent) => event.involvedObject.name, - [sortBy.count]: (event: KubeEvent) => event.count, - [sortBy.age]: (event: KubeEvent) => event.metadata.creationTimestamp, + [sortBy.namespace]: event => event.getNs(), + [sortBy.type]: event => event.involvedObject.kind, + [sortBy.object]: event => event.involvedObject.name, + [sortBy.count]: event => event.count, + [sortBy.age]: event => event.metadata.creationTimestamp, }} searchFilters={[ - (event: KubeEvent) => event.getSearchFields(), - (event: KubeEvent) => event.message, - (event: KubeEvent) => event.getSource(), - (event: KubeEvent) => event.involvedObject.name, + event => event.getSearchFields(), + event => event.message, + event => event.getSource(), + event => event.involvedObject.name, ]} renderHeaderTitle={Events} customizeHeader={({ title, info }) => ( @@ -82,7 +82,7 @@ export class Events extends React.Component { { title: Count, className: "count", sortBy: sortBy.count }, { title: Age, className: "age", sortBy: sortBy.age }, ]} - renderTableContents={(event: KubeEvent) => { + renderTableContents={event => { const { involvedObject, type, message } = event; const { kind, name } = involvedObject; const tooltipId = `message-${event.getId()}`; diff --git a/src/renderer/components/+user-management-service-accounts/service-accounts-details.tsx b/src/renderer/components/+user-management-service-accounts/service-accounts-details.tsx index 9d84d1c559..ba0a265d9d 100644 --- a/src/renderer/components/+user-management-service-accounts/service-accounts-details.tsx +++ b/src/renderer/components/+user-management-service-accounts/service-accounts-details.tsx @@ -35,16 +35,21 @@ export class ServiceAccountsDetails extends React.Component { return; } const namespace = serviceAccount.getNs(); - const secrets = serviceAccount.getSecrets().map(({ name }) => { - return secretsStore.load({ name, namespace }); - }); - this.secrets = await Promise.all(secrets); - const imagePullSecrets = serviceAccount.getImagePullSecrets().map(async({ name }) => { - return secretsStore.load({ name, namespace }).catch(() => this.generateDummySecretObject(name)); - }); - - this.imagePullSecrets = await Promise.all(imagePullSecrets); + this.secrets = await Promise.all( + serviceAccount + .getSecrets() + .map(({ name }) => secretsStore.load({ name, namespace })) + ); + this.imagePullSecrets = await Promise.all( + serviceAccount + .getImagePullSecrets() + .map(({ name }) => ( + secretsStore + .load({ name, namespace }) + .catch(() => this.generateDummySecretObject(name)) + )) + ); }); renderSecrets() { diff --git a/src/renderer/components/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx index bed38f99a3..e878f4c118 100644 --- a/src/renderer/components/+workloads-overview/overview.tsx +++ b/src/renderer/components/+workloads-overview/overview.tsx @@ -1,7 +1,7 @@ import "./overview.scss"; import React from "react"; -import { observable, when } from "mobx"; +import { computed, observable } from "mobx"; import { observer } from "mobx-react"; import { OverviewStatuses } from "./overview-statuses"; import { RouteComponentProps } from "react-router"; @@ -18,60 +18,36 @@ import { Spinner } from "../spinner"; import { Events } from "../+events"; import { KubeObjectStore } from "../../kube-object.store"; import { isAllowedResource } from "../../../common/rbac"; +import { filter } from "lodash"; interface Props extends RouteComponentProps { } @observer export class WorkloadsOverview extends React.Component { - @observable isReady = false; - @observable isUnmounting = false; + @observable stores: KubeObjectStore[] = []; + unsubscribeList: (() => void)[] = []; + + @computed get isReady() { + return this.stores.every(store => store.isLoaded); + } async componentDidMount() { - const stores: KubeObjectStore[] = []; - - if (isAllowedResource("pods")) { - stores.push(podsStore); - } - - if (isAllowedResource("deployments")) { - stores.push(deploymentStore); - } - - if (isAllowedResource("daemonsets")) { - stores.push(daemonSetStore); - } - - if (isAllowedResource("statefulsets")) { - stores.push(statefulSetStore); - } - - if (isAllowedResource("replicasets")) { - stores.push(replicaSetStore); - } - - if (isAllowedResource("jobs")) { - stores.push(jobStore); - } - - if (isAllowedResource("cronjobs")) { - stores.push(cronJobStore); - } - - if (isAllowedResource("events")) { - stores.push(eventStore); - } - this.isReady = stores.every(store => store.isLoaded); - await Promise.all(stores.map(store => store.loadAll())); - this.isReady = true; - const unsubscribeList = stores.map(store => store.subscribe()); - - await when(() => this.isUnmounting); - unsubscribeList.forEach(dispose => dispose()); + this.stores = filter([ + isAllowedResource("pods") && podsStore, + isAllowedResource("deployments") && deploymentStore, + isAllowedResource("daemonsets") && daemonSetStore, + isAllowedResource("statefulsets") && statefulSetStore, + isAllowedResource("replicasets") && replicaSetStore, + isAllowedResource("jobs") && jobStore, + isAllowedResource("cronjobs") && cronJobStore, + isAllowedResource("events") && eventStore, + ]); + this.unsubscribeList = this.stores.map(store => store.subscribe()); } componentWillUnmount() { - this.isUnmounting = true; + this.unsubscribeList.forEach(dispose => dispose()); } renderContents() { diff --git a/src/renderer/components/+workloads/workloads.stores.ts b/src/renderer/components/+workloads/workloads.stores.ts index 79828c5fd0..c7f089e2f7 100644 --- a/src/renderer/components/+workloads/workloads.stores.ts +++ b/src/renderer/components/+workloads/workloads.stores.ts @@ -8,7 +8,7 @@ import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; import { KubeResource } from "../../../common/rbac"; import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; -export const workloadStores: Partial> = { +export const workloadStores: Partial>> = { "pods": podsStore, "deployments": deploymentStore, "daemonsets": daemonSetStore, diff --git a/src/renderer/components/dock/create-resource.tsx b/src/renderer/components/dock/create-resource.tsx index b26a7385b2..798c59660e 100644 --- a/src/renderer/components/dock/create-resource.tsx +++ b/src/renderer/components/dock/create-resource.tsx @@ -46,11 +46,11 @@ export class CreateResource extends React.Component { const errors: string[] = []; await Promise.all( - resources.map(data => { - return resourceApplierApi.update(data) - .then(item => createdResources.push(item.getName())) - .catch((err: JsonApiErrorParsed) => errors.push(err.toString())); - }) + resources.map(data => ( + resourceApplierApi.update(data) + .then(item => createdResources.push(...item.map(item => item.getName()))) + .catch((err: JsonApiErrorParsed) => errors.push(err.toString())) + )) ); if (errors.length) { diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx index 963667cf98..853196b435 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -32,19 +32,19 @@ interface IHeaderPlaceholders { info: ReactNode; } -export interface ItemListLayoutProps { +export interface ItemListLayoutProps { className: IClassName; - store: ItemStore; + store: ItemStore; dependentStores?: ItemStore[]; isClusterScoped?: boolean; hideFilters?: boolean; - searchFilters?: SearchFilter[]; - filterItems?: ItemsFilter[]; + searchFilters?: SearchFilter[]; + filterItems?: ItemsFilter[]; // header (title, filtering, searching, etc.) showHeader?: boolean; headerClassName?: IClassName; - renderHeaderTitle?: ReactNode | ((parent: ItemListLayout) => ReactNode); + renderHeaderTitle?: ReactNode | ((parent: ItemListLayout) => ReactNode); customizeHeader?: (placeholders: IHeaderPlaceholders, content: ReactNode) => Partial | ReactNode; // items list configuration @@ -52,23 +52,23 @@ export interface ItemListLayoutProps { isSelectable?: boolean; // show checkbox in rows for selecting items isSearchable?: boolean; // apply search-filter & add search-input copyClassNameFromHeadCells?: boolean; - sortingCallbacks?: { [sortBy: string]: TableSortCallback }; - tableProps?: Partial>; // low-level table configuration - renderTableHeader: TableCellProps[] | null; - renderTableContents: (item: T) => (ReactNode | TableCellProps)[]; - renderItemMenu?: (item: T, store: ItemStore) => ReactNode; - customizeTableRowProps?: (item: T) => Partial; + sortingCallbacks?: Record>; + tableProps?: Partial>; // low-level table configuration + renderTableHeader: TableCellProps[] | null; + renderTableContents: (item: Entry) => (ReactNode | TableCellProps)[]; + renderItemMenu?: (item: Entry, store: ItemStore) => ReactNode; + customizeTableRowProps?: (item: Entry) => Partial; addRemoveButtons?: Partial; virtual?: boolean; // item details view hasDetailsView?: boolean; - detailsItem?: T; - onDetails?: (item: T) => void; + detailsItem?: Entry; + onDetails?: (item: Entry) => void; // other - customizeRemoveDialog?: (selectedItems: T[]) => Partial; - renderFooter?: (parent: ItemListLayout) => React.ReactNode; + customizeRemoveDialog?: (selectedItems: Entry[]) => Partial; + renderFooter?: (parent: ItemListLayout) => React.ReactNode; } const defaultProps: Partial = { diff --git a/src/renderer/components/kube-object/kube-object-details.tsx b/src/renderer/components/kube-object/kube-object-details.tsx index 2f0d2a69a7..f0a9f19ced 100644 --- a/src/renderer/components/kube-object/kube-object-details.tsx +++ b/src/renderer/components/kube-object/kube-object-details.tsx @@ -13,6 +13,7 @@ import { crdStore } from "../+custom-resources/crd.store"; import { CrdResourceDetails } from "../+custom-resources"; import { KubeObjectMenu } from "./kube-object-menu"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +import { CustomResourceDefinition } from "../../api/endpoints"; export interface KubeObjectDetailsProps { className?: string; @@ -81,7 +82,7 @@ export class KubeObjectDetails extends React.Component { }); if (isCrdInstance && details.length === 0) { - details.push(); + details.push(); } } diff --git a/src/renderer/components/kube-object/kube-object-list-layout.tsx b/src/renderer/components/kube-object/kube-object-list-layout.tsx index 0dd95328d8..842aac0b71 100644 --- a/src/renderer/components/kube-object/kube-object-list-layout.tsx +++ b/src/renderer/components/kube-object/kube-object-list-layout.tsx @@ -9,7 +9,7 @@ import { KubeObjectStore } from "../../kube-object.store"; import { KubeObjectMenu } from "./kube-object-menu"; import { ItemObject } from "../../item.store"; -export interface KubeObjectListLayoutProps extends ItemListLayoutProps { +export interface KubeObjectListLayoutProps extends ItemListLayoutProps { store: KubeObjectStore; } @@ -18,7 +18,7 @@ function showItemDetails(item: KubeObject) { } @observer -export class KubeObjectListLayout extends React.Component> { +export class KubeObjectListLayout extends React.Component> { @computed get selectedItem() { return this.props.store.getByPath(getSelectedDetails()); } diff --git a/src/renderer/components/table/table-cell.tsx b/src/renderer/components/table/table-cell.tsx index a42db4c2be..2c5da12ee4 100644 --- a/src/renderer/components/table/table-cell.tsx +++ b/src/renderer/components/table/table-cell.tsx @@ -1,5 +1,5 @@ import "./table-cell.scss"; -import type { TableSortBy, TableSortParams } from "./table"; +import type { TableSortParams } from "./table"; import React, { ReactNode } from "react"; import { autobind, cssNames, displayBooleans } from "../../utils"; @@ -8,15 +8,15 @@ import { Checkbox } from "../checkbox"; export type TableCellElem = React.ReactElement; -export interface TableCellProps extends React.DOMAttributes { +export interface TableCellProps extends React.DOMAttributes { className?: string; title?: ReactNode; checkbox?: boolean; // render cell with a checkbox isChecked?: boolean; // mark checkbox as checked or not renderBoolean?: boolean; // show "true" or "false" for all of the children elements are "typeof boolean" - sortBy?: TableSortBy; // column name, must be same as key in sortable object - _sorting?: Partial; //
sorting state, don't use this prop outside (!) - _sort?(sortBy: TableSortBy): void; //
sort function, don't use this prop outside (!) + sortBy?: SortingOption; // column name, must be same as key in sortable object
+ _sorting?: Partial>; //
sorting state, don't use this prop outside (!) + _sort?(sortBy: SortingOption): void; //
sort function, don't use this prop outside (!) _nowrap?: boolean; // indicator, might come from parent , don't use this prop outside (!) } diff --git a/src/renderer/components/table/table.tsx b/src/renderer/components/table/table.tsx index 9720a61f8c..cb25901ba6 100644 --- a/src/renderer/components/table/table.tsx +++ b/src/renderer/components/table/table.tsx @@ -14,12 +14,11 @@ import { ItemObject } from "../../item.store"; // todo: refactor + decouple search from location -export type TableSortBy = string; export type TableOrderBy = "asc" | "desc" | string; -export type TableSortParams = { sortBy: TableSortBy; orderBy: TableOrderBy }; +export type TableSortParams = { sortBy: SortingOption; orderBy: TableOrderBy }; export type TableSortCallback = (data: D) => string | number | (string | number)[]; -export interface TableProps extends React.DOMAttributes { +export interface TableProps extends React.DOMAttributes { items?: ItemObject[]; // Raw items data className?: string; autoSize?: boolean; // Setup auto-sizing for all columns (flex: 1 0) @@ -32,8 +31,8 @@ export interface TableProps extends React.DOMAttributes { [sortBy: string]: TableSortCallback; }; sortSyncWithUrl?: boolean; // sorting state is managed globally from url params - sortByDefault?: Partial; // default sorting params - onSort?: (params: TableSortParams) => void; // callback on sort change, default: global sync with url + sortByDefault?: Partial>; // default sorting params + onSort?: (params: TableSortParams) => void; // callback on sort change, default: global sync with url noItems?: React.ReactNode; // Show no items state table list is empty selectedItemId?: string; // Allows to scroll list to selected item virtual?: boolean; // Use virtual list component to render only visible rows diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index e2013f2679..06ba61dd88 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -136,12 +136,8 @@ export abstract class KubeObjectStore extends ItemStore return this.load({ name, namespace }); } - protected async createItem(params: { name: string; namespace?: string }, data?: Partial): Promise { - return this.api.create(params, data); - } - async create(params: { name: string; namespace?: string }, data?: Partial): Promise { - const newItem = await this.createItem(params, data); + const newItem = await this.api.create(params, data); const items = this.sortItems([...this.items, newItem]); this.items.replace(items); @@ -150,7 +146,7 @@ export abstract class KubeObjectStore extends ItemStore } async update(item: T, data: Partial): Promise { - const newItem = await item.update(data); + const [newItem] = await item.update(data); const index = this.items.findIndex(item => item.getId() === newItem.getId()); this.items.splice(index, 1, newItem); @@ -172,9 +168,11 @@ export abstract class KubeObjectStore extends ItemStore protected eventsBuffer = observable>([], { deep: false }); protected bindWatchEventsUpdater(delay = 1000) { - return reaction(() => this.eventsBuffer.toJS()[0], this.updateFromEventsBuffer, { - delay - }); + return reaction( + () => this.eventsBuffer.toJS()[0], + this.updateFromEventsBuffer, + { delay } + ); } subscribe(apis = [this.api]) { @@ -196,23 +194,22 @@ export abstract class KubeObjectStore extends ItemStore for (const {type, object} of this.eventsBuffer.clear()) { const { uid, selfLink } = object.metadata; - const index = items.findIndex(item => item.getId() === uid); - const item = items[index]; - const api = apiManager.getApi(selfLink); + const index = items.map(item => item.getId()).indexOf(uid); + const api = apiManager.getApi(selfLink); switch (type) { case "ADDED": case "MODIFIED": const newItem = new api.objectConstructor(object); - if (!item) { + if (index < 0) { items.push(newItem); } else { items.splice(index, 1, newItem); } break; case "DELETED": - if (item) { + if (index >= 0) { items.splice(index, 1); } break; diff --git a/src/renderer/utils/cancelableFetch.ts b/src/renderer/utils/cancelableFetch.ts index a4a197fe0d..09b40dd9a1 100644 --- a/src/renderer/utils/cancelableFetch.ts +++ b/src/renderer/utils/cancelableFetch.ts @@ -12,7 +12,7 @@ interface WrappingFunction { (result: T): T; } -export function cancelableFetch(reqInfo: RequestInfo, reqInit: RequestInit = {}) { +export function cancelableFetch(reqInfo: RequestInfo, reqInit: RequestInit = {}): CancelablePromise { const abortController = new AbortController(); const signal = abortController.signal; const cancel = abortController.abort.bind(abortController);