1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

add more typing to sorting of tables

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2020-12-15 13:29:30 -05:00
parent a62e1fc4e5
commit f66d84ae4a
17 changed files with 110 additions and 138 deletions

View File

@ -56,4 +56,4 @@ export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeOb
text: `${event.message}`,
timestamp: event.metadata.creationTimestamp
};
}
}

View File

@ -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/<username>/Library/Application Support/Lens/node_modules/@publisher/extension"
* e.g. `/Users/<username>/Library/Application Support/Lens/node_modules/@publisher/extension`
*/
protected getInstalledPath(name: string) {
return path.join(this.nodeModulesPath, name);

View File

@ -10,12 +10,12 @@ export class ApiManager {
private apis = observable.map<string, KubeApi>();
private stores = observable.map<KubeApi, KubeObjectStore<any>>();
getApi(pathOrCallback: string | ((api: KubeApi) => boolean) = () => true) {
getApi<T extends KubeObject = KubeObject>(pathOrCallback: string | ((api: KubeApi) => boolean) = () => true): KubeApi<T> {
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<T>;
}
return Array.from(this.apis.values()).find(pathOrCallback);
return Array.from(this.apis.values()).find(pathOrCallback) as KubeApi<T>;
}
registerApi(apiBase: string, api: KubeApi) {

View File

@ -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<D extends KubeObject>(resource: object | string): Promise<D> {
update<D extends KubeObject>(resource: object | string): CancelablePromise<D[]> {
if (typeof resource === "string") {
resource = jsYaml.safeLoad(resource);
}
return apiBase
.post<KubeJsonApiData[]>("/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<D>(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;
});
})
));
}
};

View File

@ -4,11 +4,8 @@ import { KubeObjectStore } from "../../kube-object.store";
import { KubeObject } from "../../api/kube-object";
@autobind()
export class CRDResourceStore<T extends KubeObject = any> extends KubeObjectStore<T> {
api: KubeApi;
constructor(api: KubeApi<T>) {
export class CRDResourceStore<T extends KubeObject = KubeObject> extends KubeObjectStore<T> {
constructor(public api: KubeApi<T>) {
super();
this.api = api;
}
}

View File

@ -22,7 +22,7 @@ enum sortBy {
age = "age",
}
interface Props extends Partial<KubeObjectListLayoutProps> {
interface Props extends Partial<KubeObjectListLayoutProps<KubeEvent>> {
className?: IClassName;
compact?: boolean;
compactLimit?: number;
@ -45,17 +45,17 @@ export class Events extends React.Component<Props> {
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={<Trans>Events</Trans>}
customizeHeader={({ title, info }) => (
@ -82,7 +82,7 @@ export class Events extends React.Component<Props> {
{ title: <Trans>Count</Trans>, className: "count", sortBy: sortBy.count },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]}
renderTableContents={(event: KubeEvent) => {
renderTableContents={event => {
const { involvedObject, type, message } = event;
const { kind, name } = involvedObject;
const tooltipId = `message-${event.getId()}`;

View File

@ -35,16 +35,21 @@ export class ServiceAccountsDetails extends React.Component<Props> {
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() {

View File

@ -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<IWorkloadsOverviewRouteParams> {
}
@observer
export class WorkloadsOverview extends React.Component<Props> {
@observable isReady = false;
@observable isUnmounting = false;
@observable stores: KubeObjectStore<any>[] = [];
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() {

View File

@ -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<Record<KubeResource, KubeObjectStore>> = {
export const workloadStores: Partial<Record<KubeResource, KubeObjectStore<any>>> = {
"pods": podsStore,
"deployments": deploymentStore,
"daemonsets": daemonSetStore,

View File

@ -46,11 +46,11 @@ export class CreateResource extends React.Component<Props> {
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) {

View File

@ -32,19 +32,19 @@ interface IHeaderPlaceholders {
info: ReactNode;
}
export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
export interface ItemListLayoutProps<Entry extends ItemObject = ItemObject, SortingOption extends string = string> {
className: IClassName;
store: ItemStore<T>;
store: ItemStore<Entry>;
dependentStores?: ItemStore[];
isClusterScoped?: boolean;
hideFilters?: boolean;
searchFilters?: SearchFilter<T>[];
filterItems?: ItemsFilter<T>[];
searchFilters?: SearchFilter<Entry>[];
filterItems?: ItemsFilter<Entry>[];
// header (title, filtering, searching, etc.)
showHeader?: boolean;
headerClassName?: IClassName;
renderHeaderTitle?: ReactNode | ((parent: ItemListLayout<T>) => ReactNode);
renderHeaderTitle?: ReactNode | ((parent: ItemListLayout<Entry>) => ReactNode);
customizeHeader?: (placeholders: IHeaderPlaceholders, content: ReactNode) => Partial<IHeaderPlaceholders> | ReactNode;
// items list configuration
@ -52,23 +52,23 @@ export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
isSelectable?: boolean; // show checkbox in rows for selecting items
isSearchable?: boolean; // apply search-filter & add search-input
copyClassNameFromHeadCells?: boolean;
sortingCallbacks?: { [sortBy: string]: TableSortCallback<T> };
tableProps?: Partial<TableProps<T>>; // low-level table configuration
renderTableHeader: TableCellProps[] | null;
renderTableContents: (item: T) => (ReactNode | TableCellProps)[];
renderItemMenu?: (item: T, store: ItemStore<T>) => ReactNode;
customizeTableRowProps?: (item: T) => Partial<TableRowProps>;
sortingCallbacks?: Record<SortingOption, TableSortCallback<Entry>>;
tableProps?: Partial<TableProps<Entry>>; // low-level table configuration
renderTableHeader: TableCellProps<SortingOption>[] | null;
renderTableContents: (item: Entry) => (ReactNode | TableCellProps)[];
renderItemMenu?: (item: Entry, store: ItemStore<Entry>) => ReactNode;
customizeTableRowProps?: (item: Entry) => Partial<TableRowProps>;
addRemoveButtons?: Partial<AddRemoveButtonsProps>;
virtual?: boolean;
// item details view
hasDetailsView?: boolean;
detailsItem?: T;
onDetails?: (item: T) => void;
detailsItem?: Entry;
onDetails?: (item: Entry) => void;
// other
customizeRemoveDialog?: (selectedItems: T[]) => Partial<ConfirmDialogParams>;
renderFooter?: (parent: ItemListLayout<T>) => React.ReactNode;
customizeRemoveDialog?: (selectedItems: Entry[]) => Partial<ConfirmDialogParams>;
renderFooter?: (parent: ItemListLayout<Entry>) => React.ReactNode;
}
const defaultProps: Partial<ItemListLayoutProps> = {

View File

@ -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<T = KubeObject> {
className?: string;
@ -81,7 +82,7 @@ export class KubeObjectDetails extends React.Component {
});
if (isCrdInstance && details.length === 0) {
details.push(<CrdResourceDetails object={object} />);
details.push(<CrdResourceDetails object={object as CustomResourceDefinition} />);
}
}

View File

@ -9,7 +9,7 @@ import { KubeObjectStore } from "../../kube-object.store";
import { KubeObjectMenu } from "./kube-object-menu";
import { ItemObject } from "../../item.store";
export interface KubeObjectListLayoutProps<T extends ItemObject & KubeObject> extends ItemListLayoutProps<T> {
export interface KubeObjectListLayoutProps<T extends ItemObject & KubeObject, SortOrder extends string> extends ItemListLayoutProps<T, SortOrder> {
store: KubeObjectStore<T>;
}
@ -18,7 +18,7 @@ function showItemDetails(item: KubeObject) {
}
@observer
export class KubeObjectListLayout<T extends ItemObject & KubeObject> extends React.Component<KubeObjectListLayoutProps<T>> {
export class KubeObjectListLayout<T extends ItemObject & KubeObject, SortOrder extends string> extends React.Component<KubeObjectListLayoutProps<T, SortOrder>> {
@computed get selectedItem() {
return this.props.store.getByPath(getSelectedDetails());
}

View File

@ -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<TableCellProps>;
export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
export interface TableCellProps<SortingOption extends string = string> extends React.DOMAttributes<HTMLDivElement> {
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 <Table sortable={}/>
_sorting?: Partial<TableSortParams>; // <Table> sorting state, don't use this prop outside (!)
_sort?(sortBy: TableSortBy): void; // <Table> sort function, don't use this prop outside (!)
sortBy?: SortingOption; // column name, must be same as key in sortable object <Table sortable={}/>
_sorting?: Partial<TableSortParams<SortingOption>>; // <Table> sorting state, don't use this prop outside (!)
_sort?(sortBy: SortingOption): void; // <Table> sort function, don't use this prop outside (!)
_nowrap?: boolean; // indicator, might come from parent <TableHead>, don't use this prop outside (!)
}

View File

@ -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<SortingOption extends string> = { sortBy: SortingOption; orderBy: TableOrderBy };
export type TableSortCallback<D> = (data: D) => string | number | (string | number)[];
export interface TableProps<T> extends React.DOMAttributes<HTMLDivElement> {
export interface TableProps<T, SortingOption extends string = string> extends React.DOMAttributes<HTMLDivElement> {
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<T> extends React.DOMAttributes<HTMLDivElement> {
[sortBy: string]: TableSortCallback<T>;
};
sortSyncWithUrl?: boolean; // sorting state is managed globally from url params
sortByDefault?: Partial<TableSortParams>; // default sorting params
onSort?: (params: TableSortParams) => void; // callback on sort change, default: global sync with url
sortByDefault?: Partial<TableSortParams<SortingOption>>; // default sorting params
onSort?: (params: TableSortParams<SortingOption>) => 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

View File

@ -136,12 +136,8 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
return this.load({ name, namespace });
}
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<T>): Promise<T> {
return this.api.create(params, data);
}
async create(params: { name: string; namespace?: string }, data?: Partial<T>): Promise<T> {
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<T extends KubeObject> extends ItemStore<T>
}
async update(item: T, data: Partial<T>): Promise<T> {
const newItem = await item.update<T>(data);
const [newItem] = await item.update<T>(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<T extends KubeObject> extends ItemStore<T>
protected eventsBuffer = observable<IKubeWatchEvent<KubeJsonApiData>>([], { 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<T extends KubeObject> extends ItemStore<T>
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<T>(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;

View File

@ -12,7 +12,7 @@ interface WrappingFunction {
<T>(result: T): T;
}
export function cancelableFetch(reqInfo: RequestInfo, reqInit: RequestInit = {}) {
export function cancelableFetch(reqInfo: RequestInfo, reqInit: RequestInit = {}): CancelablePromise<any> {
const abortController = new AbortController();
const signal = abortController.signal;
const cancel = abortController.abort.bind(abortController);