Turn on strict mode in tsconfig.json, some helpful lints, and required cleanup where strictness necessitates it (#5195)

* Turn on strict mode in tsconfig.json

- Add route, clusterRoute, and payloadValidatedClusterRoute helper
  functions to improve types with backend routes

- Turn on the following new lints:
  - react/jsx-first-prop-new-line
  - react/jsx-wrap-multilines
  - react/jsx-one-expression-per-line
  - react/jsx-max-props-per-line
  - react/jsx-indent
  - react/jsx-indent-props

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix build

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace KubeObject scope strings with enum

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Revert package.json version changes

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* revert move hostedCluster(Id)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* change some type param names to be not single letters

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove copy-extension-themes

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* add new make clean action

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix build to not use webpack for generating types

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kube-object-menu.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix select.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix catalog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* revert move fileNameMigration to index

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix ref logic error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix log-resource-selector.test.tsx tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix dock-store.test.ts test by overriding createStorage to not touch file system

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix cluster.test.ts tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kube=api.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fixed hotbar-store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kubeconfig-manager.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix cluster-role-bindings/__tests__/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix role-bindings/__tests__/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix pods.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix delete-cluster-dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix daemonset.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix replicaset.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix statefulsets/dialog/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix replicasets/scale-dialog/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix deployments.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix deployments/scale/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix cronjob.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix stateful-set.api.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix deployment.api.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix api-manager.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix statefulset.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix job.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix pods.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix scroll-spy.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix hotbar-remove-command.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix catalog-entity-registry.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix welcome.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix verify-that-all-routes-have-route-component.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix pod-tolerations.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* better fix for previous 3 fixes, plus also select.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kube-object-menu.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix app-paths.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix dock-tabs.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix isReactNode typing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix sub-title.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix drawer.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix list-layout.tsx and header.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix error-boundary.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix upgrade-chart/store.ts and dock-tab.store.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix install-chart/store.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix edit-resource/store.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix create-resource/store.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix namespace-select.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix namespace-select-filter.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix crd-list.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix wrong types for extensions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix circular dependency

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix circular dependency on catalogCategoryRegistry

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix api-kube

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix type errors, most <Select /> errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fixing more type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* some more fixing type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* convert all KubeApis to injectable with legacy global backups

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* factor out into a common file all the exports

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* convert all KubeObjectStores to injectable with legacy global backups

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix lint

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove unused legacy KubeApi globals

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix bad previous commit

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* more crash fixing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* try and fix behavioural tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix sidebar-and-tab-navigation-for-core.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix sidebar-and-tab-navigation-for-extensions.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-using-application-menu.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix catalog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make ThemeStore non-singleton and fix navigation-to-terminal-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* extensions.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix catalog-entity-registry.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-using-application-menu.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix log-resource-selector.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix dock-tabs.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix delete-cluster-dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-kubernetes-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-editor-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-proxy-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-using-application-menu.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-application-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix dock-store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix select.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix role-bindings/__tests__/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix hotbar-remove-command.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix cluster-role-bindings/__tests__/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-extension-specific-preferences.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-telemetry-preferences.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix closing-preferences.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-editor-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-proxy-preferences.test.ts

- Fix other type errors too

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* final tweaks

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add more tsconfig files, fix bug in <Catalog>

- Make all of history, navigation injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert all of kube-details-params/ and navigate/ to injectable

- This fixes a runtime error that was encountered during testing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix runtime errors on renderer

- remove all static uses of `createPageParam` (and then removed the
  legacy global)
- Made LensRendererExtension and LensMainExtension just used
  dependencies and not the getLegacyDi
- Fixed circular dep in extension-loader

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move registerStore calls to after injectMany

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* replace all the rest of the legacy uses of apiManager

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix stack overflow and cycles in DI

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix NamespaceSelectFilter not opening

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix WizardStep and AddNamespaceDialog

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix KubeApi's not being registered

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* cleanup WindowManager

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Proper fix for Wizard, fix NamespaceStore.subscribe

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Rewrite withTooltip to be more type correct

- Fixes mobx related "too many recursive actions" error

- Change all the uses of withTooltips to be functional components

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add e2e test to cover kube api registration

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* cleanup internal-commands

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove cast in <Animate>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix command-palette e2e test

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type error after rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix test name

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix lint

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix code to help CodeQL scanner

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* update intree extension lock files

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix build-extensions picking wrong @types/react

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix tests from rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix type error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make KubeconfigSyncManager more injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix crash in test mode for Dialog

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* make Select snapshots deterministic

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix new type error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kube-object.store.test.ts typing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix merge build issues

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix snapshots after merge

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix lint after merge

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* reexport BaseKubeJsonApiObjectMetadata

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix typo in terminalSpawningPool

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove duplicate license header

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix typo to waitUntilDefined

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove iter use from getLegacyGlobalDiForExtensionApi

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove complex createStorage override

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* override logger with mocks only when needed for tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove specialized overrideStore flags for getDiForUnitTesting

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove unnecessary | undefined types from the exactOptionalFieldTypes experiment

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* use more descriptive name for local test mocks

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove unnecessary addition to 'make clean' target

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove oddity of KubeObjectStore.getById(undefined) being allowed

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* rename KubeObject.getDescriptor in favour of name without fundemental JS meaning

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Simplify legacyRegisterApi when working in behaviour unit tests

- Don't emit within main environment as there should be no auto
  registering there

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* change confusing variable name in ReactiveDuration

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* make visitor pattern more explicit for Entity contextMenuOpen

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* toggleDetails -> toggleKubeDetailsPane is more specific

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove outdated comment

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix bug where LensExtension dependencies are not set

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests from the revert of react 18

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix more tests from merge

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix typings with new is-compatible-extension tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* more type fixing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Revert in-tree extension versions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Improve name of guarding injectable for stores and apis

- New name better implies that it is just a guard state and does not do
  anything

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add helper for <Select>.isMulti for storing in a Set<Value>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix is-compatible-extension.test.ts types

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2022-05-16 07:17:57 -04:00
1 parent 381d77c633
commit dfcb7c3330
1062 files changed
+26512 -28689

No files matched your search

@@ -13,49 +13,51 @@ import type { ConfirmDialogParams } from "../confirm-dialog";
import type { TableCellProps, TableProps, TableRowProps, TableSortCallbacks } from "../table";
import { Table, TableCell, TableHead, TableRow } from "../table";
import type { IClassName } from "../../utils";
import { autoBind, cssNames, isReactNode, prevDefault, stopPropagation } from "../../utils";
import { autoBind, cssNames, isDefined, isReactNode, noop, prevDefault, stopPropagation } from "../../utils";
import type { AddRemoveButtonsProps } from "../add-remove-buttons";
import { AddRemoveButtons } from "../add-remove-buttons";
import { NoItems } from "../no-items";
import { Spinner } from "../spinner";
import type { ItemObject, ItemStore } from "../../../common/item.store";
import type { Filter } from "./page-filters.store";
import { pageFilters } from "./page-filters.store";
import { ThemeStore } from "../../theme.store";
import type { ItemObject } from "../../../common/item.store";
import type { Filter, PageFiltersStore } from "./page-filters/store";
import type { ThemeStore } from "../../themes/store";
import { MenuActions } from "../menu/menu-actions";
import { MenuItem } from "../menu";
import { Checkbox } from "../checkbox";
import { UserStore } from "../../../common/user-store";
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
import type { ItemListStore } from "./list-layout";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import pageFiltersStoreInjectable from "./page-filters/store.injectable";
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
import openConfirmDialogInjectable from "../confirm-dialog/open.injectable";
export interface ItemListLayoutContentProps<I extends ItemObject> {
export interface ItemListLayoutContentProps<Item extends ItemObject, PreLoadStores extends boolean> {
getFilters: () => Filter[];
tableId?: string;
className: IClassName;
getItems: () => I[];
store: ItemStore<I>;
getItems: () => Item[];
store: ItemListStore<Item, PreLoadStores>;
getIsReady: () => boolean; // show loading indicator while not ready
isSelectable?: boolean; // show checkbox in rows for selecting items
isConfigurable?: boolean;
copyClassNameFromHeadCells?: boolean;
sortingCallbacks?: TableSortCallbacks<I>;
tableProps?: Partial<TableProps<I>>; // low-level table configuration
renderTableHeader: TableCellProps[] | null;
renderTableContents: (item: I) => (ReactNode | TableCellProps)[];
renderItemMenu?: (item: I, store: ItemStore<I>) => ReactNode;
customizeTableRowProps?: (item: I) => Partial<TableRowProps>;
sortingCallbacks?: TableSortCallbacks<Item>;
tableProps?: Partial<TableProps<Item>>; // low-level table configuration
renderTableHeader?: (TableCellProps | undefined | null)[];
renderTableContents: (item: Item) => (ReactNode | TableCellProps)[];
renderItemMenu?: (item: Item, store: ItemListStore<Item, PreLoadStores>) => ReactNode;
customizeTableRowProps?: (item: Item) => Partial<TableRowProps<Item>>;
addRemoveButtons?: Partial<AddRemoveButtonsProps>;
virtual?: boolean;
// item details view
hasDetailsView?: boolean;
detailsItem?: I;
onDetails?: (item: I) => void;
detailsItem?: Item;
onDetails?: (item: Item) => void;
// other
customizeRemoveDialog?: (selectedItems: I[]) => Partial<ConfirmDialogParams>;
customizeRemoveDialog?: (selectedItems: Item[]) => Partial<ConfirmDialogParams>;
/**
* Message to display when a store failed to load
@@ -66,12 +68,17 @@ export interface ItemListLayoutContentProps<I extends ItemObject> {
}
interface Dependencies {
themeStore: ThemeStore;
pageFiltersStore: PageFiltersStore;
openConfirmDialog: OpenConfirmDialog;
}
@observer
class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Component<ItemListLayoutContentProps<I> & Dependencies> {
constructor(props: ItemListLayoutContentProps<I> & Dependencies) {
class NonInjectedItemListLayoutContent<
Item extends ItemObject,
PreLoadStores extends boolean,
> extends React.Component<ItemListLayoutContentProps<Item, PreLoadStores> & Dependencies> {
constructor(props: ItemListLayoutContentProps<Item, PreLoadStores> & Dependencies) {
super(props);
makeObservable(this);
autoBind(this);
@@ -81,15 +88,15 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
return this.props.store.failedLoading;
}
renderRow(item: I) {
renderRow(item: Item) {
return this.getTableRow(item);
}
getTableRow(item: I) {
getTableRow(item: Item) {
const {
isSelectable, renderTableHeader, renderTableContents, renderItemMenu,
store, hasDetailsView, onDetails,
copyClassNameFromHeadCells, customizeTableRowProps, detailsItem,
copyClassNameFromHeadCells, customizeTableRowProps = () => ({}), detailsItem,
} = this.props;
const { isSelected } = store;
@@ -99,7 +106,7 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
searchItem={item}
sortItem={item}
selected={detailsItem && detailsItem.getId() === item.getId()}
onClick={hasDetailsView ? prevDefault(() => onDetails(item)) : undefined}
onClick={hasDetailsView ? prevDefault(() => onDetails?.(item)) : undefined}
{...customizeTableRowProps(item)}
>
{isSelectable && (
@@ -155,7 +162,7 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
);
}
removeItemsDialog(selectedItems: I[]) {
removeItemsDialog(selectedItems: Item[]) {
const { customizeRemoveDialog, store, openConfirmDialog } = this.props;
const visibleMaxNamesCount = 5;
const selectedNames = selectedItems.map(ns => ns.getName()).slice(0, visibleMaxNamesCount).join(", ");
@@ -165,14 +172,38 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
? selectedCount - visibleMaxNamesCount
: 0;
const tail = tailCount > 0
? <>, and <b>{tailCount}</b> more</>
? (
<>
{", and "}
<b>{tailCount}</b>
{" more"}
</>
)
: null;
const message = selectedCount <= 1
? <p>Remove item <b>{selectedNames}</b>?</p>
: <p>Remove <b>{selectedCount}</b> items <b>{selectedNames}</b>{tail}?</p>;
const onConfirm = store.removeItems
? () => store.removeItems(selectedItems)
: store.removeSelectedItems;
? (
<p>
{"Remove item "}
<b>{selectedNames}</b>
?
</p>
)
: (
<p>
Remove
<b>{selectedCount}</b>
{" items "}
<b>{selectedNames}</b>
{tail}
?
</p>
);
const { removeSelectedItems, removeItems } = store;
const onConfirm = typeof removeItems === "function"
? () => removeItems.apply(store, [selectedItems])
: typeof removeSelectedItems === "function"
? removeSelectedItems.bind(store)
: noop;
openConfirmDialog({
ok: onConfirm,
@@ -196,7 +227,7 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
<NoItems>
No items found.
<p>
<a onClick={() => pageFilters.reset()} className="contrast">
<a onClick={() => this.props.pageFiltersStore.reset()} className="contrast">
Reset filters?
</a>
</p>
@@ -222,7 +253,7 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
return null;
}
const enabledItems = this.props.getItems().filter(item => !customizeTableRowProps(item).disabled);
const enabledItems = this.props.getItems().filter(item => !customizeTableRowProps?.(item).disabled);
return (
<TableHead showTopLine nowrap>
@@ -237,11 +268,15 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
)}
</Observer>
)}
{renderTableHeader.map((cellProps, index) => (
this.showColumn(cellProps) && (
<TableCell key={cellProps.id ?? index} {...cellProps} />
)
))}
{
renderTableHeader
.filter(isDefined)
.map((cellProps, index) => (
this.showColumn(cellProps) && (
<TableCell key={cellProps.id ?? index} {...cellProps} />
)
))
}
<TableCell className="menu">
{isConfigurable && this.renderColumnVisibilityMenu()}
</TableCell>
@@ -252,10 +287,10 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
render() {
const {
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks,
detailsItem, className, tableProps = {}, tableId, getItems,
detailsItem, className, tableProps = {}, tableId, getItems, themeStore,
} = this.props;
const selectedItemId = detailsItem && detailsItem.getId();
const classNames = cssNames(className, "box", "grow", ThemeStore.getInstance().activeTheme.type);
const classNames = cssNames(className, "box", "grow", themeStore.activeTheme.type);
const items = getItems();
const selectedItems = store.pickOnlySelected(items);
@@ -284,7 +319,7 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
onRemove={
(store.removeItems || store.removeSelectedItems) && selectedItems.length > 0
? () => this.removeItemsDialog(selectedItems)
: null
: undefined
}
removeTooltip={`Remove selected items (${selectedItems.length})`}
{...addRemoveButtons}
@@ -298,33 +333,47 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
showColumn({ id: columnId, showWithColumn }: TableCellProps): boolean {
const { tableId, isConfigurable } = this.props;
return !isConfigurable || !UserStore.getInstance().isTableColumnHidden(tableId, columnId, showWithColumn);
return !isConfigurable || !tableId || !UserStore.getInstance().isTableColumnHidden(tableId, columnId, showWithColumn);
}
renderColumnVisibilityMenu() {
const { renderTableHeader, tableId } = this.props;
const { renderTableHeader = [], tableId } = this.props;
return (
<MenuActions className="ItemListLayoutVisibilityMenu" toolbar={false} autoCloseOnSelect={false}>
{renderTableHeader.map((cellProps, index) => (
!cellProps.showWithColumn && (
<MenuItem key={index} className="input">
<Checkbox
label={cellProps.title ?? `<${cellProps.className}>`}
value={this.showColumn(cellProps)}
onChange={() => UserStore.getInstance().toggleTableColumnVisibility(tableId, cellProps.id)}
/>
</MenuItem>
)
))}
<MenuActions
className="ItemListLayoutVisibilityMenu"
toolbar={false}
autoCloseOnSelect={false}
>
{
renderTableHeader
.filter(isDefined)
.map((cellProps, index) => (
!cellProps.showWithColumn && (
<MenuItem key={index} className="input">
<Checkbox
label={cellProps.title ?? `<${cellProps.className}>`}
value={this.showColumn(cellProps)}
onChange={(
tableId
? (() => cellProps.id && UserStore.getInstance().toggleTableColumnVisibility(tableId, cellProps.id))
: undefined
)}
/>
</MenuItem>
)
))
}
</MenuActions>
);
}
}
export const ItemListLayoutContent = withInjectables<Dependencies, ItemListLayoutContentProps<ItemObject>>(NonInjectedItemListLayoutContent, {
export const ItemListLayoutContent = withInjectables<Dependencies, ItemListLayoutContentProps<ItemObject, boolean>>(NonInjectedItemListLayoutContent, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
pageFiltersStore: di.inject(pageFiltersStoreInjectable),
openConfirmDialog: di.inject(openConfirmDialogInjectable),
}),
}) as <I extends ItemObject>(props: ItemListLayoutContentProps<I>) => React.ReactElement;
}) as <Item extends ItemObject, PreLoadStores extends boolean>(props: ItemListLayoutContentProps<Item, PreLoadStores>) => React.ReactElement;
@@ -6,7 +6,7 @@
import React from "react";
import type { IconProps } from "../icon";
import { Icon } from "../icon";
import { FilterType } from "./page-filters.store";
import { FilterType } from "./page-filters/store";
export interface FilterIconProps extends Partial<IconProps> {
type: FilterType;
@@ -17,9 +17,21 @@ export function FilterIcon(props: FilterIconProps) {
switch (type) {
case FilterType.SEARCH:
return <Icon small material="search" {...iconProps}/>;
return (
<Icon
small
material="search"
{...iconProps}
/>
);
default:
return <Icon small material="filter_list" {...iconProps}/>;
return (
<Icon
small
material="filter_list"
{...iconProps}
/>
);
}
}
@@ -6,9 +6,9 @@
import "./item-list-layout.scss";
import React from "react";
import { PageFiltersList } from "./page-filters-list";
import { PageFiltersList } from "./page-filters/list";
import { observer } from "mobx-react";
import type { Filter } from "./page-filters.store";
import type { Filter } from "./page-filters/store";
export interface ItemListLayoutFilterProps {
getIsReady: () => boolean;
@@ -9,18 +9,18 @@ import type { ReactNode } from "react";
import React from "react";
import { observer } from "mobx-react";
import type { IClassName } from "../../utils";
import { cssNames } from "../../utils";
import type { ItemObject, ItemStore } from "../../../common/item.store";
import type { Filter } from "./page-filters.store";
import type { HeaderCustomizer, HeaderPlaceholders, SearchFilter } from "./list-layout";
import { cssNames, isDefined } from "../../utils";
import type { ItemObject } from "../../../common/item.store";
import type { Filter } from "./page-filters/store";
import type { HeaderCustomizer, HeaderPlaceholders, ItemListStore, SearchFilter } from "./list-layout";
import { SearchInputUrl } from "../input";
export interface ItemListLayoutHeaderProps<I extends ItemObject> {
export interface ItemListLayoutHeaderProps<I extends ItemObject, PreLoadStores extends boolean> {
getItems: () => I[];
getFilters: () => Filter[];
toggleFilters: () => void;
store: ItemStore<I>;
store: ItemListStore<I, PreLoadStores>;
searchFilters?: SearchFilter<I>[];
// header (title, filtering, searching, etc.)
@@ -28,21 +28,19 @@ export interface ItemListLayoutHeaderProps<I extends ItemObject> {
headerClassName?: IClassName;
renderHeaderTitle?:
| ReactNode
| ((parent: ItemListLayoutHeader<I>) => ReactNode);
| (() => ReactNode);
customizeHeader?: HeaderCustomizer | HeaderCustomizer[];
}
@observer
export class ItemListLayoutHeader<I extends ItemObject> extends React.Component<
ItemListLayoutHeaderProps<I>
> {
export class ItemListLayoutHeader<I extends ItemObject, PreLoadStores extends boolean> extends React.Component<ItemListLayoutHeaderProps<I, PreLoadStores>> {
render() {
const {
showHeader,
customizeHeader,
renderHeaderTitle,
headerClassName,
searchFilters,
searchFilters = [],
getItems,
store,
getFilters,
@@ -60,7 +58,8 @@ export class ItemListLayoutHeader<I extends ItemObject> extends React.Component<
if (getFilters().length > 0) {
return (
<>
<a onClick={toggleFilters}>Filtered</a>: {itemsCount} / {allItemsCount}
<a onClick={toggleFilters}>Filtered</a>
{`: ${itemsCount} / ${allItemsCount}`}
</>
);
}
@@ -70,9 +69,9 @@ export class ItemListLayoutHeader<I extends ItemObject> extends React.Component<
: `${allItemsCount} items`;
};
const customizeHeaderFunctions = [customizeHeader].flat().filter(Boolean);
const customizeHeaderFunctions = [customizeHeader].flat().filter(isDefined);
const renderedTitle = typeof renderHeaderTitle === "function"
? renderHeaderTitle(this)
? renderHeaderTitle()
: renderHeaderTitle;
const {
@@ -7,18 +7,18 @@ import "./item-list-layout.scss";
import type { ReactNode } from "react";
import React from "react";
import type { IComputedValue } from "mobx";
import { computed, makeObservable, untracked } from "mobx";
import type { ConfirmDialogParams } from "../confirm-dialog";
import type { TableCellProps, TableProps, TableRowProps, TableSortCallbacks } from "../table";
import type { IClassName, StorageHelper } from "../../utils";
import type { IClassName, SingleOrMany, StorageLayer } from "../../utils";
import { autoBind, cssNames, noop } from "../../utils";
import type { AddRemoveButtonsProps } from "../add-remove-buttons";
import type { ItemObject, ItemStore } from "../../../common/item.store";
import type { ItemObject } from "../../../common/item.store";
import type { SearchInputUrlProps } from "../input";
import { FilterType, pageFilters } from "./page-filters.store";
import { PageFiltersList } from "./page-filters-list";
import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store";
import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable";
import type { PageFiltersStore } from "./page-filters/store";
import { FilterType } from "./page-filters/store";
import { PageFiltersList } from "./page-filters/list";
import { withInjectables } from "@ogre-tools/injectable-react";
import itemListLayoutStorageInjectable from "./storage.injectable";
import { ItemListLayoutContent } from "./content";
@@ -26,8 +26,12 @@ import { ItemListLayoutHeader } from "./header";
import groupBy from "lodash/groupBy";
import { ItemListLayoutFilters } from "./filters";
import { observer } from "mobx-react";
import type { Primitive } from "type-fest";
import type { SubscribableStore } from "../../kube-watch-api/kube-watch-api";
import selectedFilterNamespacesInjectable from "../../../common/k8s-api/selected-filter-namespaces.injectable";
import pageFiltersStoreInjectable from "./page-filters/store.injectable";
export type SearchFilter<I extends ItemObject> = (item: I) => string | number | (string | number)[];
export type SearchFilter<I extends ItemObject> = (item: I) => SingleOrMany<string | number | undefined | null>;
export type SearchFilters<I extends ItemObject> = Record<string, SearchFilter<I>>;
export type ItemsFilter<I extends ItemObject> = (items: I[]) => I[];
export type ItemsFilters<I extends ItemObject> = Record<string, ItemsFilter<I>>;
@@ -39,23 +43,59 @@ export interface HeaderPlaceholders {
info?: ReactNode;
}
function normalizeText(value: Primitive) {
return String(value).toLowerCase();
}
export type ItemListStore<I extends ItemObject, PreLoadStores extends boolean> = {
readonly isLoaded: boolean;
readonly failedLoading: boolean;
getTotalCount: () => number;
isSelected: (item: I) => boolean;
toggleSelection: (item: I) => void;
isSelectedAll: (items: I[]) => boolean;
toggleSelectionAll: (enabledItems: I[]) => void;
pickOnlySelected: (items: I[]) => I[];
} & ({
removeItems: (selectedItems: I[]) => Promise<void>;
readonly selectedItems: I[];
removeSelectedItems?: unknown;
} | {
removeSelectedItems: () => Promise<void>;
selectedItems?: unknown;
removeItems?: unknown;
}) & (
PreLoadStores extends true
? {
loadAll: (selectedNamespaces: readonly string[]) => Promise<void>;
}
: {
loadAll?: unknown;
}
);
export type RenderHeaderTitle<
Item extends ItemObject,
PreLoadStores extends boolean,
> = ReactNode | ((parent: NonInjectedItemListLayout<Item, PreLoadStores>) => ReactNode);
export type HeaderCustomizer = (placeholders: HeaderPlaceholders) => HeaderPlaceholders;
export interface ItemListLayoutProps<I extends ItemObject> {
export type ItemListLayoutProps<Item extends ItemObject, PreLoadStores extends boolean = boolean> = {
tableId?: string;
className: IClassName;
getItems: () => I[];
store: ItemStore<I>;
dependentStores?: ItemStore<ItemObject>[];
getItems: () => Item[];
store: ItemListStore<Item, PreLoadStores>;
dependentStores?: SubscribableStore[];
preloadStores?: boolean;
hideFilters?: boolean;
searchFilters?: SearchFilter<I>[];
searchFilters?: SearchFilter<Item>[];
/** @deprecated */
filterItems?: ItemsFilter<I>[];
filterItems?: ItemsFilter<Item>[];
// header (title, filtering, searching, etc.)
showHeader?: boolean;
headerClassName?: IClassName;
renderHeaderTitle?: ReactNode | ((parent: NonInjectedItemListLayout<I>) => ReactNode);
renderHeaderTitle?: RenderHeaderTitle<Item, PreLoadStores>;
customizeHeader?: HeaderCustomizer | HeaderCustomizer[];
// items list configuration
@@ -63,23 +103,23 @@ export interface ItemListLayoutProps<I extends ItemObject> {
isSelectable?: boolean; // show checkbox in rows for selecting items
isConfigurable?: boolean;
copyClassNameFromHeadCells?: boolean;
sortingCallbacks?: TableSortCallbacks<I>;
tableProps?: Partial<TableProps<I>>; // low-level table configuration
renderTableHeader: TableCellProps[] | null;
renderTableContents: (item: I) => (ReactNode | TableCellProps)[];
renderItemMenu?: (item: I, store: ItemStore<I>) => ReactNode;
customizeTableRowProps?: (item: I) => Partial<TableRowProps>;
sortingCallbacks?: TableSortCallbacks<Item>;
tableProps?: Partial<TableProps<Item>>; // low-level table configuration
renderTableHeader?: (TableCellProps | undefined | null)[];
renderTableContents: (item: Item) => (ReactNode | TableCellProps)[];
renderItemMenu?: (item: Item, store: ItemListStore<Item, PreLoadStores>) => ReactNode;
customizeTableRowProps?: (item: Item) => Partial<TableRowProps<Item>>;
addRemoveButtons?: Partial<AddRemoveButtonsProps>;
virtual?: boolean;
// item details view
hasDetailsView?: boolean;
detailsItem?: I;
onDetails?: (item: I) => void;
detailsItem?: Item;
onDetails?: (item: Item) => void;
// other
customizeRemoveDialog?: (selectedItems: I[]) => Partial<ConfirmDialogParams>;
renderFooter?: (parent: NonInjectedItemListLayout<I>) => React.ReactNode;
customizeRemoveDialog?: (selectedItems: Item[]) => Partial<ConfirmDialogParams>;
renderFooter?: (parent: NonInjectedItemListLayout<Item, PreLoadStores>) => React.ReactNode;
/**
* Message to display when a store failed to load
@@ -88,10 +128,18 @@ export interface ItemListLayoutProps<I extends ItemObject> {
*/
failedToLoadMessage?: React.ReactNode;
filterCallbacks?: ItemsFilters<I>;
}
filterCallbacks?: ItemsFilters<Item>;
} & (
PreLoadStores extends true
? {
preloadStores?: true;
}
: {
preloadStores: false;
}
);
const defaultProps: Partial<ItemListLayoutProps<ItemObject>> = {
const defaultProps: Partial<ItemListLayoutProps<ItemObject, true>> = {
showHeader: true,
isSelectable: true,
isConfigurable: false,
@@ -108,16 +156,21 @@ const defaultProps: Partial<ItemListLayoutProps<ItemObject>> = {
failedToLoadMessage: "Failed to load items",
};
export interface ItemListLayoutStorage {
showFilters: boolean;
}
interface Dependencies {
namespaceStore: NamespaceStore;
itemListLayoutStorage: StorageHelper<{ showFilters: boolean }>;
selectedFilterNamespaces: IComputedValue<string[]>;
itemListLayoutStorage: StorageLayer<ItemListLayoutStorage>;
pageFiltersStore: PageFiltersStore;
}
@observer
class NonInjectedItemListLayout<I extends ItemObject> extends React.Component<ItemListLayoutProps<I> & Dependencies> {
class NonInjectedItemListLayout<I extends ItemObject, PreLoadStores extends boolean> extends React.Component<ItemListLayoutProps<I, PreLoadStores> & Dependencies> {
static defaultProps = defaultProps as object;
constructor(props: ItemListLayoutProps<I> & Dependencies) {
constructor(props: ItemListLayoutProps<I, PreLoadStores> & Dependencies) {
super(props);
makeObservable(this);
autoBind(this);
@@ -131,17 +184,13 @@ class NonInjectedItemListLayout<I extends ItemObject> extends React.Component<It
}
if (preloadStores) {
this.loadStores();
const { store, dependentStores = [] } = this.props;
const stores = Array.from(new Set([store, ...dependentStores])) as ItemListStore<I, true>[];
stores.forEach(store => store.loadAll(this.props.selectedFilterNamespaces.get()));
}
}
private loadStores() {
const { store, dependentStores } = this.props;
const stores = Array.from(new Set([store, ...dependentStores]));
stores.forEach(store => store.loadAll(this.props.namespaceStore.contextNamespaces));
}
get showFilters(): boolean {
return this.props.itemListLayoutStorage.get().showFilters;
}
@@ -151,8 +200,8 @@ class NonInjectedItemListLayout<I extends ItemObject> extends React.Component<It
}
@computed get filters() {
let { activeFilters } = pageFilters;
const { searchFilters } = this.props;
let { activeFilters } = this.props.pageFiltersStore;
const { searchFilters = [] } = this.props;
if (searchFilters.length === 0) {
activeFilters = activeFilters.filter(({ type }) => type !== FilterType.SEARCH);
@@ -182,20 +231,20 @@ class NonInjectedItemListLayout<I extends ItemObject> extends React.Component<It
private filterCallbacks: ItemsFilters<I> = {
[FilterType.SEARCH]: items => {
const { searchFilters } = this.props;
const search = pageFilters.getValues(FilterType.SEARCH)[0] || "";
const { searchFilters = [] } = this.props;
const search = this.props.pageFiltersStore.getValues(FilterType.SEARCH)[0] || "";
if (search && searchFilters.length) {
const normalizeText = (text: string) => String(text).toLowerCase();
const searchTexts = [search].map(normalizeText);
return items.filter(item => {
return searchFilters.some(getTexts => {
const sourceTexts: string[] = [getTexts(item)].flat().map(normalizeText);
return sourceTexts.some(source => searchTexts.some(search => source.includes(search)));
});
});
return items.filter(item => (
searchFilters.some(getTexts => (
[getTexts(item)]
.flat()
.map(normalizeText)
.some(source => searchTexts.some(search => source.includes(search)))
))
));
}
return items;
@@ -216,10 +265,12 @@ class NonInjectedItemListLayout<I extends ItemObject> extends React.Component<It
const items = this.props.getItems();
return applyFilters(filterItems.concat(this.props.filterItems), items);
return applyFilters(filterItems.concat(this.props.filterItems ?? []), items);
}
render() {
const { renderHeaderTitle } = this.props;
return untracked(() => (
<div
className={cssNames("ItemListLayout flex column", this.props.className)}
@@ -232,7 +283,11 @@ class NonInjectedItemListLayout<I extends ItemObject> extends React.Component<It
searchFilters={this.props.searchFilters}
showHeader={this.props.showHeader}
headerClassName={this.props.headerClassName}
renderHeaderTitle={this.props.renderHeaderTitle}
renderHeaderTitle={(
typeof renderHeaderTitle === "function"
? () => renderHeaderTitle(this)
: renderHeaderTitle
)}
customizeHeader={this.props.customizeHeader}
/>
@@ -240,10 +295,10 @@ class NonInjectedItemListLayout<I extends ItemObject> extends React.Component<It
getIsReady={() => this.isReady}
getFilters={() => this.filters}
getFiltersAreShown={() => this.showFilters}
hideFilters={this.props.hideFilters}
hideFilters={this.props.hideFilters ?? false}
/>
<ItemListLayoutContent
<ItemListLayoutContent<I, PreLoadStores>
getItems={() => this.items}
getFilters={() => this.filters}
tableId={this.props.tableId}
@@ -274,17 +329,14 @@ class NonInjectedItemListLayout<I extends ItemObject> extends React.Component<It
}
}
const InjectedItemListLayout = withInjectables<Dependencies, ItemListLayoutProps<ItemObject>>(NonInjectedItemListLayout, {
export const ItemListLayout = withInjectables<Dependencies, ItemListLayoutProps<ItemObject, boolean>>(NonInjectedItemListLayout, {
getProps: (di, props) => ({
namespaceStore: di.inject(namespaceStoreInjectable),
itemListLayoutStorage: di.inject(itemListLayoutStorageInjectable),
...props,
selectedFilterNamespaces: di.inject(selectedFilterNamespacesInjectable),
itemListLayoutStorage: di.inject(itemListLayoutStorageInjectable),
pageFiltersStore: di.inject(pageFiltersStoreInjectable),
}),
});
export function ItemListLayout<I extends ItemObject>(props: ItemListLayoutProps<I>) {
return <InjectedItemListLayout {...props} />;
}
}) as <I extends ItemObject, PreLoadStores extends boolean = true>(props: ItemListLayoutProps<I, PreLoadStores>) => React.ReactElement;
function applyFilters<I extends ItemObject>(filters: ItemsFilter<I>[], items: I[]): I[] {
if (!filters || !filters.length) {
@@ -1,85 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./page-filters-list.scss";
import React from "react";
import { observer } from "mobx-react";
import { Badge } from "../badge";
import { cssNames } from "../../utils";
import type { Filter } from "./page-filters.store";
import { pageFilters } from "./page-filters.store";
import { FilterIcon } from "./filter-icon";
import { Icon } from "../icon";
import { searchUrlParam } from "../input";
export interface PageFiltersListProps {
filters?: Filter[];
}
@observer
export class PageFiltersList extends React.Component<PageFiltersListProps> {
static defaultProps: PageFiltersListProps = {
get filters() {
return pageFilters.activeFilters;
},
};
reset = () => pageFilters.reset();
remove = (filter: Filter) => {
pageFilters.removeFilter(filter);
searchUrlParam.clear();
};
renderContent() {
const { filters } = this.props;
if (!filters.length) {
return null;
}
return (
<>
<div className="header flex gaps">
<span>Currently applied filters:</span>
<a onClick={this.reset} className="reset">
Reset
</a>
</div>
<div className="labels">
{filters.map(filter => {
const { value, type } = filter;
return (
<Badge
key={`${type}-${value}`}
title={type}
className={cssNames("Badge flex gaps filter align-center", type)}
label={(
<>
<FilterIcon type={type}/>
<span className="value">{value}</span>
<Icon
small
material="close"
onClick={() => this.remove(filter)}
/>
</>
)}
/>
);
})}
</div>
</>
);
}
render() {
return (
<div className="PageFiltersList">
{this.renderContent()}
</div>
);
}
}
@@ -0,0 +1,95 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./list.scss";
import React from "react";
import { observer } from "mobx-react";
import { Badge } from "../../badge";
import { cssNames } from "../../../utils";
import type { Filter, PageFiltersStore } from "./store";
import { FilterIcon } from "../filter-icon";
import { Icon } from "../../icon";
import type { PageParam } from "../../../navigation";
import { withInjectables } from "@ogre-tools/injectable-react";
import pageFiltersStoreInjectable from "./store.injectable";
import searchUrlPageParamInjectable from "../../input/search-url-page-param.injectable";
export interface PageFiltersListProps {
filters?: Filter[];
}
interface Dependencies {
pageFiltersStore: PageFiltersStore;
searchUrlParam: PageParam<string>;
}
const NonInjectedPageFiltersList = observer(({
pageFiltersStore,
searchUrlParam,
filters: rawFilters,
}: Dependencies & PageFiltersListProps) => {
const filters = rawFilters ?? pageFiltersStore.activeFilters;
const reset = () => pageFiltersStore.reset();
const remove = (filter: Filter) => {
pageFiltersStore.removeFilter(filter);
searchUrlParam.clear();
};
const renderContent = () => {
if (filters.length === 0) {
return null;
}
return (
<>
<div className="header flex gaps">
<span>Currently applied filters:</span>
<a onClick={reset} className="reset">
Reset
</a>
</div>
<div className="labels">
{filters.map(filter => {
const { value, type } = filter;
return (
<Badge
key={`${type}-${value}`}
title={type}
className={cssNames("Badge flex gaps filter align-center", type)}
label={(
<>
<FilterIcon type={type}/>
<span className="value">{value}</span>
<Icon
small
material="close"
onClick={() => remove(filter)}
/>
</>
)}
/>
);
})}
</div>
</>
);
};
return (
<div className="PageFiltersList">
{renderContent()}
</div>
);
});
export const PageFiltersList = withInjectables<Dependencies, PageFiltersListProps>(NonInjectedPageFiltersList, {
getProps: (di, props) => ({
...props,
pageFiltersStore: di.inject(pageFiltersStoreInjectable),
searchUrlParam: di.inject(searchUrlPageParamInjectable),
}),
});
@@ -0,0 +1,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import searchUrlPageParamInjectable from "../../input/search-url-page-param.injectable";
import { PageFiltersStore } from "./store";
const pageFiltersStoreInjectable = getInjectable({
id: "page-filters-store",
instantiate: (di) => new PageFiltersStore({
searchUrlParam: di.inject(searchUrlPageParamInjectable),
}),
});
export default pageFiltersStoreInjectable;
@@ -4,8 +4,8 @@
*/
import { computed, observable, reaction, makeObservable, action } from "mobx";
import { autoBind } from "../../utils";
import { searchUrlParam } from "../input/search-input-url";
import type { PageParam } from "../../../navigation";
import { autoBind } from "../../../utils";
export enum FilterType {
SEARCH = "search",
@@ -16,6 +16,10 @@ export interface Filter {
value: string;
}
interface Dependencies {
readonly searchUrlParam: PageParam<string>;
}
export class PageFiltersStore {
protected filters = observable.array<Filter>([], { deep: false });
protected isDisabled = observable.map<FilterType, boolean>();
@@ -24,7 +28,7 @@ export class PageFiltersStore {
return this.filters.filter(filter => !this.isDisabled.get(filter.type));
}
constructor() {
constructor(protected readonly dependencies: Dependencies) {
makeObservable(this);
autoBind(this);
@@ -33,8 +37,8 @@ export class PageFiltersStore {
protected syncWithGlobalSearch() {
const disposers = [
reaction(() => this.getValues(FilterType.SEARCH)[0], search => searchUrlParam.set(search)),
reaction(() => searchUrlParam.get(), search => {
reaction(() => this.getValues(FilterType.SEARCH)[0], search => this.dependencies.searchUrlParam.set(search)),
reaction(() => this.dependencies.searchUrlParam.get(), search => {
const filter = this.getByType(FilterType.SEARCH);
if (filter) {
@@ -69,7 +73,7 @@ export class PageFiltersStore {
}
}
getByType(type: FilterType, value?: any): Filter {
getByType(type: FilterType, value?: any): Filter | undefined {
return this.filters.find(filter => filter.type === type && (
arguments.length > 1 ? filter.value === value : true
));
@@ -102,12 +106,10 @@ export class PageFiltersStore {
@action
reset() {
if (this.isEnabled(FilterType.SEARCH)) {
searchUrlParam.clear();
this.dependencies.searchUrlParam.clear();
}
this.filters.length = 0;
this.isDisabled.clear();
}
}
export const pageFilters = new PageFiltersStore();