/** * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ import EventEmitter from "events"; import type TypedEmitter from "typed-emitter"; import { observable, makeObservable } from "mobx"; import { once } from "lodash"; import type { Disposer } from "../utils"; import { iter } from "../utils"; import type { CategoryColumnRegistration } from "../../renderer/components/+catalog/custom-category-columns"; export type CatalogEntityDataFor = Entity extends CatalogEntity ? CatalogEntityData : never; export type CatalogEntityInstanceFrom = Constructor extends CatalogEntityConstructor ? Entity : never; export type CatalogEntityConstructor = ( new (data: CatalogEntityDataFor) => Entity ); export interface CatalogCategoryVersion { /** * The specific version that the associated constructor is for. This MUST be * a DNS label and SHOULD be of the form `vN`, `vNalphaY`, or `vNbetaY` where * `N` and `Y` are both integers greater than 0. * * Examples: The following are valid values for this field. * - `v1` * - `v1beta1` * - `v1alpha2` * - `v3beta2` */ readonly name: string; /** * The constructor for the entities. */ readonly entityClass: CatalogEntityConstructor; } export interface CatalogCategorySpec { /** * The grouping for for the category. This MUST be a DNS label. */ readonly group: string; /** * The specific versions of the constructors. * * NOTE: the field `.apiVersion` after construction MUST match `{.group}/{.versions.[] | .name}`. * For example, if `group = "entity.k8slens.dev"` and there is an entry in `.versions` with * `name = "v1alpha1"` then the resulting `.apiVersion` MUST be `entity.k8slens.dev/v1alpha1` */ readonly versions: CatalogCategoryVersion[]; /** * This is the concerning the category */ readonly names: { /** * The kind of entity that this category is for. This value MUST be a DNS * label and MUST be equal to the `kind` fields that are produced by the * `.versions.[] | .entityClass` fields. */ readonly kind: string; }; /** * These are the columns used for displaying entities when in the catalog. * * If this is not provided then some default columns will be used, similar in * scope to the columns in the "Browse" view. * * Even if you provide columns, a "Name" column will be provided as well with * `priority: 0`. * * These columns will not be used in the "Browse" view. */ readonly displayColumns?: CategoryColumnRegistration[]; } /** * If the filter return a thruthy value, the menu item is displayed */ export type AddMenuFilter = (menu: CatalogEntityAddMenu) => any; export interface CatalogCategoryEvents { /** * This event will be emitted when the category is loaded in the catalog * view. */ load: () => void; /** * This event will be emitted when the catalog add menu is opened and is the * way to added entries to that menu. */ catalogAddMenu: (context: CatalogEntityAddMenuContext) => void; /** * This event will be emitted when the context menu for an entity is declared * by this category is opened. */ contextMenuOpen: (entity: CatalogEntity, context: CatalogEntityContextMenuContext) => void; } export interface CatalogCategoryMetadata { /** * The name of your category. The category can be searched for by this * value. This will also be used for the catalog menu. */ readonly name: string; /** * Either an `` or the name of an icon from {@link IconProps} */ readonly icon: string; } export function categoryVersion< T extends CatalogEntity, Metadata extends CatalogEntityMetadata, Status extends CatalogEntityStatus, Spec extends CatalogEntitySpec, >(name: string, entityClass: new (data: CatalogEntityData) => T): CatalogCategoryVersion { return { name, entityClass: entityClass as CatalogEntityConstructor, }; } export abstract class CatalogCategory extends (EventEmitter as new () => TypedEmitter) { /** * The version of category that you are wanting to declare. * * Currently supported values: * * - `"catalog.k8slens.dev/v1alpha1"` */ abstract readonly apiVersion: string; /** * The kind of item you wish to declare. * * Currently supported values: * * - `"CatalogCategory"` */ abstract readonly kind: string; /** * The data about the category itself */ abstract readonly metadata: CatalogCategoryMetadata; /** * The most important part of a category, as it is where entity versions are declared. */ abstract readonly spec: CatalogCategorySpec; /** * @internal */ protected readonly filters = observable.set([], { deep: false, }); /** * Parse a category ID into parts. * @param id The id of a category is parse * @returns The group and kind parts of the ID */ public static parseId(id: string): { group?: string; kind?: string } { const [group, kind] = id.split("/") ?? []; return { group, kind }; } /** * Get the ID of this category */ public getId(): string { return `${this.spec.group}/${this.spec.names.kind}`; } /** * Get the name of this category */ public getName(): string { return this.metadata.name; } /** * Get the badge of this category. * Defaults to no badge. * The badge is displayed next to the Category name in the Catalog Category menu */ public getBadge(): React.ReactNode { return null; } /** * Add a filter for menu items of catalogAddMenu * @param fn The function that should return a truthy value if that menu item should be displayed * @returns A function to remove that filter */ public addMenuFilter(fn: AddMenuFilter): Disposer { this.filters.add(fn); return once(() => void this.filters.delete(fn)); } /** * Filter menuItems according to the Category's set filters * @param menuItems menu items to filter * @returns filtered menu items */ public filteredItems(menuItems: CatalogEntityAddMenu[]) { return Array.from( iter.reduce( this.filters, iter.filter, menuItems.values(), ), ); } } export type EntityMetadataObject = { [Key in string]?: EntityMetadataValue }; export type EntityMetadataValue = string | number | boolean | EntityMetadataObject | undefined; export interface CatalogEntityMetadata extends EntityMetadataObject { uid: string; name: string; shortName?: string; description?: string; source?: string; labels: Record; } export interface CatalogEntityStatus { phase: string; reason?: string; /** * @default true */ enabled?: boolean; message?: string; active?: boolean; } export interface CatalogEntityActionContext { navigate: (url: string) => void; setCommandPaletteContext: (context?: CatalogEntity) => void; } export interface CatalogEntityContextMenu { /** * Menu title */ title: string; /** * Menu icon */ icon?: string; /** * OnClick handler */ onClick: () => void | Promise; /** * Confirm click with a message */ confirm?: { message: string; }; } export interface CatalogEntityAddMenu extends CatalogEntityContextMenu { icon: string; defaultAction?: boolean; } export interface CatalogEntitySettingsMenu { group?: string; title: string; components: { View: React.ComponentType; }; } export interface CatalogEntityContextMenuNavigate { /** * @param pathname The location to navigate to in the main iframe */ (pathname: string, forceMainFrame?: boolean): void; /** * @param pathname The location to navigate to in the current iframe. Useful for when called within the cluster frame */ (pathname: string, forceMainFrame: false): void; } export interface CatalogEntityContextMenuContext { /** * Navigate to the specified pathname */ navigate: CatalogEntityContextMenuNavigate; menuItems: CatalogEntityContextMenu[]; } export interface CatalogEntitySettingsContext { menuItems: CatalogEntityContextMenu[]; } export interface CatalogEntityAddMenuContext { navigate: (url: string) => void; menuItems: CatalogEntityAddMenu[]; } export type CatalogEntitySpec = Record; export interface CatalogEntityData< Metadata extends CatalogEntityMetadata = CatalogEntityMetadata, Status extends CatalogEntityStatus = CatalogEntityStatus, Spec extends CatalogEntitySpec = CatalogEntitySpec, > { metadata: Metadata; status: Status; spec: Spec; } export interface CatalogEntityKindData { readonly apiVersion: string; readonly kind: string; } export abstract class CatalogEntity< Metadata extends CatalogEntityMetadata = CatalogEntityMetadata, Status extends CatalogEntityStatus = CatalogEntityStatus, Spec extends CatalogEntitySpec = CatalogEntitySpec, > implements CatalogEntityKindData { /** * The group and version of this class. */ public abstract readonly apiVersion: string; /** * A DNS label name of the entity. */ public abstract readonly kind: string; @observable metadata: Metadata; @observable status: Status; @observable spec: Spec; constructor({ metadata, status, spec }: CatalogEntityData) { makeObservable(this); if (!metadata || typeof metadata !== "object") { throw new TypeError("CatalogEntity's metadata must be a defined object"); } if (!status || typeof status !== "object") { throw new TypeError("CatalogEntity's status must be a defined object"); } if (!spec || typeof spec !== "object") { throw new TypeError("CatalogEntity's spec must be a defined object"); } this.metadata = metadata; this.status = status; this.spec = spec; } /** * Get the UID of this entity */ public getId(): string { return this.metadata.uid; } /** * Get the name of this entity */ public getName(): string { return this.metadata.name; } /** * Get the specified source of this entity, defaulting to `"unknown"` if not * provided */ public getSource(): string { return this.metadata.source ?? "unknown"; } /** * Get if this entity is enabled. */ public isEnabled(): boolean { return this.status.enabled ?? true; } public onRun?(context: CatalogEntityActionContext): void | Promise; public onContextMenuOpen?(context: CatalogEntityContextMenuContext): void | Promise; public onSettingsOpen?(context: CatalogEntitySettingsContext): void | Promise; }