mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add support for custom columns on catalog categories
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
d31ab690c2
commit
ec31105681
91
src/common/utils/collection-functions.ts
Normal file
91
src/common/utils/collection-functions.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value behind `key`. If it was not pressent, first insert `value`
|
||||||
|
* @param map The map to interact with
|
||||||
|
* @param key The key to insert into the map with
|
||||||
|
* @param value The value to optional add to the map
|
||||||
|
* @returns The value in the map
|
||||||
|
*/
|
||||||
|
export function getOrInsert<K, V>(map: Map<K, V>, key: K, value: V): V {
|
||||||
|
if (map.has(key)) {
|
||||||
|
return map.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map.set(key, value).get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value behind `key`. If it was not pressent, first insert the value returned by `getVal`
|
||||||
|
*
|
||||||
|
* This function should be used over `getInsertWith` if it is expensive to build a `V`
|
||||||
|
* @param map The map to interact with
|
||||||
|
* @param key The key to insert into the map with
|
||||||
|
* @param builder A function that returns a new instance of `V`.
|
||||||
|
* @returns The value in the map
|
||||||
|
*/
|
||||||
|
export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, builder: () => V): V {
|
||||||
|
if (map.has(key)) {
|
||||||
|
return map.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map.set(key, builder()).get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value associated with `key` iff there was not a previous value
|
||||||
|
* @param map The map to interact with
|
||||||
|
* @throws if `key` already in map
|
||||||
|
* @returns `this` so that `strictSet` can be chained
|
||||||
|
*/
|
||||||
|
export function strictSet<K, V>(map: Map<K, V>, key: K, val: V): typeof map {
|
||||||
|
if (map.has(key)) {
|
||||||
|
throw new TypeError("Duplicate key in map");
|
||||||
|
}
|
||||||
|
|
||||||
|
return map.set(key, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value associated with `key`
|
||||||
|
* @param map The map to interact with
|
||||||
|
* @throws if `key` did not a value associated with it
|
||||||
|
*/
|
||||||
|
export function strictGet<K, V>(map: Map<K, V>, key: K): V {
|
||||||
|
if (!map.has(key)) {
|
||||||
|
throw new TypeError("key not in map");
|
||||||
|
}
|
||||||
|
|
||||||
|
return map.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the "has" in `set` of `value`
|
||||||
|
* @param set The set to interact with
|
||||||
|
* @param value The value to either add if not present or remove if present from `set`
|
||||||
|
*/
|
||||||
|
export function toggle<T>(set: Set<T>, value: T): void {
|
||||||
|
if (!set.delete(value)) {
|
||||||
|
// Set.prototype.delete returns false if `value` was not in the set
|
||||||
|
set.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,11 +10,19 @@ export function noop<T extends any[]>(...args: T): void {
|
|||||||
return void args;
|
return void args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A typecorrect version of <function>.bind()
|
||||||
|
*/
|
||||||
|
export function bind<BoundArgs extends any[], NonBoundArgs extends any[], ReturnType>(fn: (...args: [...BoundArgs, ...NonBoundArgs]) => ReturnType, thisArg: any, ...boundArgs: BoundArgs): (...args: NonBoundArgs) => ReturnType {
|
||||||
|
return fn.bind(thisArg, ...boundArgs);
|
||||||
|
}
|
||||||
|
|
||||||
export * from "./app-version";
|
export * from "./app-version";
|
||||||
export * from "./autobind";
|
export * from "./autobind";
|
||||||
export * from "./camelCase";
|
export * from "./camelCase";
|
||||||
export * from "./cloneJson";
|
export * from "./cloneJson";
|
||||||
export * from "./cluster-id-url-parsing";
|
export * from "./cluster-id-url-parsing";
|
||||||
|
export * from "./collection-functions";
|
||||||
export * from "./convertCpu";
|
export * from "./convertCpu";
|
||||||
export * from "./convertMemory";
|
export * from "./convertMemory";
|
||||||
export * from "./debouncePromise";
|
export * from "./debouncePromise";
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import type { WelcomeMenuRegistration } from "../renderer/components/+welcome/we
|
|||||||
import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration";
|
import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration";
|
||||||
import type { CommandRegistration } from "../renderer/components/command-palette/registered-commands/commands";
|
import type { CommandRegistration } from "../renderer/components/command-palette/registered-commands/commands";
|
||||||
import type { AppPreferenceRegistration } from "../renderer/components/+preferences/app-preferences/app-preference-registration";
|
import type { AppPreferenceRegistration } from "../renderer/components/+preferences/app-preferences/app-preference-registration";
|
||||||
|
import type { AdditionalCategoryColumnRegistration } from "../renderer/components/+catalog/custom-category-columns";
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: registries.PageRegistration[] = [];
|
globalPages: registries.PageRegistration[] = [];
|
||||||
@ -33,6 +34,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
welcomeBanners: WelcomeBannerRegistration[] = [];
|
welcomeBanners: WelcomeBannerRegistration[] = [];
|
||||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||||
topBarItems: TopBarRegistration[] = [];
|
topBarItems: TopBarRegistration[] = [];
|
||||||
|
additionalCategoryColumns: AdditionalCategoryColumnRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||||
const { navigate } = await import("../renderer/navigation");
|
const { navigate } = await import("../renderer/navigation");
|
||||||
|
|||||||
@ -28,25 +28,18 @@ import { RenderDelay } from "../render-delay/render-delay";
|
|||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
|
import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
|
||||||
import { Avatar } from "../avatar";
|
import { Avatar } from "../avatar";
|
||||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
|
||||||
import { getLabelBadges } from "./helpers";
|
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import catalogPreviousActiveTabStorageInjectable
|
import catalogPreviousActiveTabStorageInjectable from "./catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
|
||||||
from "./catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
|
|
||||||
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable";
|
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable";
|
||||||
|
import type { GetCategoryColumnsParams, CategoryColumns } from "./get-category-columns.injectable";
|
||||||
enum sortBy {
|
import getCategoryColumnsInjectable from "./get-category-columns.injectable";
|
||||||
name = "name",
|
|
||||||
kind = "kind",
|
|
||||||
source = "source",
|
|
||||||
status = "status",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<CatalogViewRouteParam> {}
|
interface Props extends RouteComponentProps<CatalogViewRouteParam> {}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
catalogPreviousActiveTabStorage: { set: (value: string ) => void }
|
catalogPreviousActiveTabStorage: { set: (value: string ) => void };
|
||||||
catalogEntityStore: CatalogEntityStore
|
catalogEntityStore: CatalogEntityStore;
|
||||||
|
getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -228,46 +221,23 @@ class NonInjectedCatalog extends React.Component<Props & Dependencies> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { sortingCallbacks, searchFilters, renderTableContents, renderTableHeader } = this.props.getCategoryColumns({ activeCategory });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemListLayout
|
<ItemListLayout
|
||||||
className={styles.Catalog}
|
className={styles.Catalog}
|
||||||
tableId={tableId}
|
tableId={tableId}
|
||||||
renderHeaderTitle={activeCategory?.metadata.name || "Browse All"}
|
renderHeaderTitle={activeCategory?.metadata.name ?? "Browse All"}
|
||||||
isSelectable={false}
|
isSelectable={false}
|
||||||
isConfigurable={true}
|
isConfigurable={true}
|
||||||
store={this.props.catalogEntityStore}
|
store={this.props.catalogEntityStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={sortingCallbacks}
|
||||||
[sortBy.name]: entity => entity.getName(),
|
searchFilters={searchFilters}
|
||||||
[sortBy.source]: entity => entity.getSource(),
|
renderTableHeader={renderTableHeader}
|
||||||
[sortBy.status]: entity => entity.status.phase,
|
|
||||||
[sortBy.kind]: entity => entity.kind,
|
|
||||||
}}
|
|
||||||
searchFilters={[
|
|
||||||
entity => [
|
|
||||||
entity.getName(),
|
|
||||||
entity.getId(),
|
|
||||||
entity.status.phase,
|
|
||||||
`source=${entity.getSource()}`,
|
|
||||||
...KubeObject.stringifyLabels(entity.metadata.labels),
|
|
||||||
],
|
|
||||||
]}
|
|
||||||
renderTableHeader={[
|
|
||||||
{ title: "Name", className: styles.entityName, sortBy: sortBy.name, id: "name" },
|
|
||||||
!activeCategory && { title: "Kind", sortBy: sortBy.kind, id: "kind" },
|
|
||||||
{ title: "Source", className: styles.sourceCell, sortBy: sortBy.source, id: "source" },
|
|
||||||
{ title: "Labels", className: `${styles.labelsCell} scrollable`, id: "labels" },
|
|
||||||
{ title: "Status", className: styles.statusCell, sortBy: sortBy.status, id: "status" },
|
|
||||||
].filter(Boolean)}
|
|
||||||
customizeTableRowProps={entity => ({
|
customizeTableRowProps={entity => ({
|
||||||
disabled: !entity.isEnabled(),
|
disabled: !entity.isEnabled(),
|
||||||
})}
|
})}
|
||||||
renderTableContents={entity => [
|
renderTableContents={renderTableContents}
|
||||||
this.renderName(entity),
|
|
||||||
!activeCategory && entity.kind,
|
|
||||||
entity.getSource(),
|
|
||||||
getLabelBadges(entity),
|
|
||||||
<span key="phase" className={entity.status.phase}>{entity.status.phase}</span>,
|
|
||||||
].filter(Boolean)}
|
|
||||||
onDetails={this.onDetails}
|
onDetails={this.onDetails}
|
||||||
renderItemMenu={this.renderItemMenu}
|
renderItemMenu={this.renderItemMenu}
|
||||||
/>
|
/>
|
||||||
@ -306,17 +276,11 @@ class NonInjectedCatalog extends React.Component<Props & Dependencies> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Catalog = withInjectables<Dependencies, Props>(
|
export const Catalog = withInjectables<Dependencies, Props>( NonInjectedCatalog, {
|
||||||
NonInjectedCatalog,
|
getProps: (di, props) => ({
|
||||||
{
|
catalogEntityStore: di.inject(catalogEntityStoreInjectable),
|
||||||
getProps: (di, props) => ({
|
catalogPreviousActiveTabStorage: di.inject(catalogPreviousActiveTabStorageInjectable),
|
||||||
catalogEntityStore: di.inject(catalogEntityStoreInjectable),
|
getCategoryColumns: di.inject(getCategoryColumnsInjectable),
|
||||||
|
...props,
|
||||||
catalogPreviousActiveTabStorage: di.inject(
|
}),
|
||||||
catalogPreviousActiveTabStorageInjectable,
|
});
|
||||||
),
|
|
||||||
|
|
||||||
...props,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { computed, IComputedValue } from "mobx";
|
||||||
|
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
|
||||||
|
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
|
||||||
|
import { getOrInsert } from "../../utils";
|
||||||
|
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
extensions: IComputedValue<LensRendererExtension[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdditionCategoryColumns({ extensions }: Dependencies): IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>> {
|
||||||
|
return computed(() => {
|
||||||
|
const res = new Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>();
|
||||||
|
|
||||||
|
for (const ext of extensions.get()) {
|
||||||
|
for (const { renderCell, titleProps, priority = 50, searchFilter, sortCallback, ...registration } of ext.additionalCategoryColumns) {
|
||||||
|
const byGroup = getOrInsert(res, registration.group, new Map<string, RegisteredAdditionalCategoryColumn[]>());
|
||||||
|
const byKind = getOrInsert(byGroup, registration.kind, []);
|
||||||
|
const id = `${ext.name}:${registration.id}`;
|
||||||
|
|
||||||
|
byKind.push({
|
||||||
|
renderCell,
|
||||||
|
priority,
|
||||||
|
id,
|
||||||
|
titleProps: {
|
||||||
|
id,
|
||||||
|
...titleProps,
|
||||||
|
sortBy: sortCallback
|
||||||
|
? id
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
searchFilter,
|
||||||
|
sortCallback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryColumnsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => getAdditionCategoryColumns({
|
||||||
|
extensions: di.inject(rendererExtensionsInjectable),
|
||||||
|
}),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default categoryColumnsInjectable;
|
||||||
99
src/renderer/components/+catalog/custom-category-columns.ts
Normal file
99
src/renderer/components/+catalog/custom-category-columns.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from "react";
|
||||||
|
import type { CatalogEntity } from "../../../common/catalog";
|
||||||
|
import type { TableCellProps } from "../table";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are the supported props for the title cell
|
||||||
|
*/
|
||||||
|
export interface TitleCellProps {
|
||||||
|
className?: string;
|
||||||
|
title?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the type used to declare new catalog category columns
|
||||||
|
*/
|
||||||
|
export interface AdditionalCategoryColumnRegistration {
|
||||||
|
/**
|
||||||
|
* The catalog entity kind that is declared by the category for this registration
|
||||||
|
*
|
||||||
|
* e.g.
|
||||||
|
* - `"KubernetesCluster"`
|
||||||
|
*/
|
||||||
|
kind: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The catalog entity group that is declared by the category for this registration
|
||||||
|
*
|
||||||
|
* e.g.
|
||||||
|
* - `"entity.k8slens.dev"`
|
||||||
|
*/
|
||||||
|
group: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sorting order value.
|
||||||
|
*
|
||||||
|
* @default 50
|
||||||
|
*/
|
||||||
|
priority?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This value MUST to be unique to your extension
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will be called to generate the cells (on demand) for the column
|
||||||
|
*/
|
||||||
|
renderCell: (entity: CatalogEntity) => React.ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function will be used to generate the columns title cell.
|
||||||
|
*/
|
||||||
|
titleProps: TitleCellProps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If provided then the column will support sorting and this function will be called to
|
||||||
|
* determine a row's ordering.
|
||||||
|
*
|
||||||
|
* strings are sorted ahead of numbers, and arrays determine ordering between equal
|
||||||
|
* elements of the previous index.
|
||||||
|
*/
|
||||||
|
sortCallback?: (entity: CatalogEntity) => string | number | (string | number)[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If provided then searching is supported on this column and this function will be called
|
||||||
|
* to determine if the current search string matches for this row.
|
||||||
|
*/
|
||||||
|
searchFilter?: (entity: CatalogEntity) => string | string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisteredAdditionalCategoryColumn {
|
||||||
|
id: string;
|
||||||
|
priority: number;
|
||||||
|
renderCell: (entity: CatalogEntity) => React.ReactNode;
|
||||||
|
titleProps: TableCellProps;
|
||||||
|
sortCallback?: (entity: CatalogEntity) => string | number | (string | number)[];
|
||||||
|
searchFilter?: (entity: CatalogEntity) => string | string[];
|
||||||
|
}
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { orderBy } from "lodash";
|
||||||
|
import type { IComputedValue } from "mobx";
|
||||||
|
import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
|
||||||
|
import { bind } from "../../utils";
|
||||||
|
import type { ItemListLayoutProps } from "../item-object-list";
|
||||||
|
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||||
|
import categoryColumnsInjectable from "./custom-category-columns.injectable";
|
||||||
|
import { anyCategoryColumns, browseAllColumns } from "./internal-category-columns";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
extensionColumns: IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetCategoryColumnsParams {
|
||||||
|
activeCategory: CatalogCategory | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CategoryColumns = Required<Pick<ItemListLayoutProps<CatalogEntity>, "sortingCallbacks" | "searchFilters" | "renderTableContents" | "renderTableHeader">>;
|
||||||
|
|
||||||
|
function getNonGlobalColums(activeCategory: CatalogCategory | null | undefined, extensionColumns: IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>>) {
|
||||||
|
if (activeCategory) {
|
||||||
|
return extensionColumns
|
||||||
|
.get()
|
||||||
|
.get(activeCategory.spec.group)
|
||||||
|
?.get(activeCategory.spec.names.kind)
|
||||||
|
?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return browseAllColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategoryColumns({ extensionColumns }: Dependencies, { activeCategory }: GetCategoryColumnsParams): CategoryColumns {
|
||||||
|
const allRegistrations = orderBy(
|
||||||
|
[
|
||||||
|
...getNonGlobalColums(activeCategory, extensionColumns),
|
||||||
|
...anyCategoryColumns,
|
||||||
|
],
|
||||||
|
"priority",
|
||||||
|
"asc",
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortingCallbacks: CategoryColumns["sortingCallbacks"] = {};
|
||||||
|
const searchFilters: CategoryColumns["searchFilters"] = [];
|
||||||
|
const renderTableHeader: CategoryColumns["renderTableHeader"] = [];
|
||||||
|
const tableRowRenderers: ((entity: CatalogEntity) => React.ReactNode)[] = [];
|
||||||
|
|
||||||
|
for (const registration of allRegistrations) {
|
||||||
|
if (registration.sortCallback) {
|
||||||
|
sortingCallbacks[registration.id] = registration.sortCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registration.searchFilter) {
|
||||||
|
searchFilters.push(registration.searchFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
tableRowRenderers.push(registration.renderCell);
|
||||||
|
renderTableHeader.push(registration.titleProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortingCallbacks,
|
||||||
|
renderTableHeader,
|
||||||
|
renderTableContents: entity => tableRowRenderers.map(fn => fn(entity)),
|
||||||
|
searchFilters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryColumnsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => bind(getCategoryColumns, null, {
|
||||||
|
extensionColumns: di.inject(categoryColumnsInjectable),
|
||||||
|
}),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getCategoryColumnsInjectable;
|
||||||
136
src/renderer/components/+catalog/internal-category-columns.tsx
Normal file
136
src/renderer/components/+catalog/internal-category-columns.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import styles from "./catalog.module.scss";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { HotbarStore } from "../../../common/hotbar-store";
|
||||||
|
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||||
|
import { Avatar } from "../avatar";
|
||||||
|
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { prevDefault } from "../../utils";
|
||||||
|
import { getLabelBadges } from "./helpers";
|
||||||
|
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||||
|
|
||||||
|
function renderEntityName(entity: CatalogEntity) {
|
||||||
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
const isItemInHotbar = hotbarStore.isAddedToActive(entity);
|
||||||
|
const onClick = prevDefault(
|
||||||
|
isItemInHotbar
|
||||||
|
? () => hotbarStore.removeFromHotbar(entity.getId())
|
||||||
|
: () => hotbarStore.addToHotbar(entity),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Avatar
|
||||||
|
title={entity.getName()}
|
||||||
|
colorHash={`${entity.getName()}-${entity.getSource()}`}
|
||||||
|
src={entity.spec.icon?.src}
|
||||||
|
background={entity.spec.icon?.background}
|
||||||
|
className={styles.catalogAvatar}
|
||||||
|
size={24}
|
||||||
|
>
|
||||||
|
{entity.spec.icon?.material && <Icon material={entity.spec.icon?.material} small/>}
|
||||||
|
</Avatar>
|
||||||
|
<span>{entity.getName()}</span>
|
||||||
|
<Icon
|
||||||
|
small
|
||||||
|
className={styles.pinIcon}
|
||||||
|
material={!isItemInHotbar && "push_pin"}
|
||||||
|
svg={isItemInHotbar ? "push_off" : "push_pin"}
|
||||||
|
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const browseAllColumns: RegisteredAdditionalCategoryColumn[] = [
|
||||||
|
{
|
||||||
|
id: "kind",
|
||||||
|
priority: 5,
|
||||||
|
renderCell: entity => entity.kind,
|
||||||
|
titleProps: {
|
||||||
|
id: "kind",
|
||||||
|
sortBy: "kind",
|
||||||
|
title: "Kind",
|
||||||
|
},
|
||||||
|
sortCallback: entity => entity.kind,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const anyCategoryColumns: RegisteredAdditionalCategoryColumn[] = [
|
||||||
|
{
|
||||||
|
id: "name",
|
||||||
|
priority: 0,
|
||||||
|
renderCell: renderEntityName,
|
||||||
|
titleProps: {
|
||||||
|
title: "Name",
|
||||||
|
className: styles.entityName,
|
||||||
|
id: "name",
|
||||||
|
sortBy: "name",
|
||||||
|
},
|
||||||
|
searchFilter: entity => entity.getName(),
|
||||||
|
sortCallback: entity => `name=${entity.getName()}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "source",
|
||||||
|
priority: 10,
|
||||||
|
renderCell: entity => entity.getSource(),
|
||||||
|
titleProps: {
|
||||||
|
title: "Source",
|
||||||
|
className: styles.sourceCell,
|
||||||
|
id: "source",
|
||||||
|
sortBy: "source",
|
||||||
|
},
|
||||||
|
sortCallback: entity => entity.getSource(),
|
||||||
|
searchFilter: entity => `source=${entity.getSource()}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "labels",
|
||||||
|
priority: 20,
|
||||||
|
renderCell: getLabelBadges,
|
||||||
|
titleProps: {
|
||||||
|
id: "labels",
|
||||||
|
title: "Labels",
|
||||||
|
className: `${styles.labelsCell} scrollable`,
|
||||||
|
},
|
||||||
|
searchFilter: entity => KubeObject.stringifyLabels(entity.metadata.labels),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "status",
|
||||||
|
priority: 30,
|
||||||
|
renderCell: entity => (
|
||||||
|
<span key="phase" className={entity.status.phase}>
|
||||||
|
{entity.status.phase}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
titleProps: {
|
||||||
|
title: "Status",
|
||||||
|
className: styles.statusCell,
|
||||||
|
id: "status",
|
||||||
|
sortBy: "status",
|
||||||
|
},
|
||||||
|
searchFilter: entity => entity.status.phase,
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -14,18 +14,66 @@ import { Checkbox } from "../checkbox";
|
|||||||
export type TableCellElem = React.ReactElement<TableCellProps>;
|
export type TableCellElem = React.ReactElement<TableCellProps>;
|
||||||
|
|
||||||
export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
|
export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
|
||||||
id?: string; // used for configuration visibility of columns
|
/**
|
||||||
|
* used for configuration visibility of columns
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any css class names for this table cell. Only used if `title` is a "simple" react node
|
||||||
|
*/
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual value of the cell
|
||||||
|
*/
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
scrollable?: boolean; // content inside could be scrolled
|
|
||||||
checkbox?: boolean; // render cell with a checkbox
|
/**
|
||||||
isChecked?: boolean; // mark checkbox as checked or not
|
* content inside could be scrolled
|
||||||
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={}/>
|
scrollable?: boolean;
|
||||||
showWithColumn?: string // id of the column which follow same visibility rules
|
|
||||||
_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 (!)
|
* render cell with a checkbox
|
||||||
_nowrap?: boolean; // indicator, might come from parent <TableHead>, don't use this prop outside (!)
|
*/
|
||||||
|
checkbox?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mark checkbox as checked or not
|
||||||
|
*/
|
||||||
|
isChecked?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* show "true" or "false" for all of the children elements are "typeof boolean"
|
||||||
|
*/
|
||||||
|
renderBoolean?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* column name, must be same as key in sortable object <Table sortable={}/>
|
||||||
|
*/
|
||||||
|
sortBy?: TableSortBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* id of the column which follow same visibility rules
|
||||||
|
*/
|
||||||
|
showWithColumn?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_sorting?: Partial<TableSortParams>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_sort?(sortBy: TableSortBy): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* indicator, might come from parent <TableHead>, don't use this prop outside (!)
|
||||||
|
*/
|
||||||
|
_nowrap?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TableCell extends React.Component<TableCellProps> {
|
export class TableCell extends React.Component<TableCellProps> {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user