mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Spread extensions to smaller components
Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
parent
34bce1764d
commit
38343af2ef
@ -1,52 +1,12 @@
|
|||||||
.PageLayout.Extensions {
|
.PageLayout.Extensions {
|
||||||
$spacing: $padding * 2;
|
$spacing: $padding * 2;
|
||||||
--width: 50%;
|
width: 100%;
|
||||||
|
|
||||||
h2 {
|
.contentRegion {
|
||||||
margin-bottom: $padding;
|
.content {
|
||||||
}
|
max-width: 740px;
|
||||||
|
|
||||||
.no-extensions {
|
|
||||||
--flex-gap: #{$padding};
|
|
||||||
padding: $padding;
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-size: $font-size-small;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-extension {
|
|
||||||
margin: $spacing * 2 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.installed-extensions {
|
|
||||||
--flex-gap: #{$spacing};
|
|
||||||
|
|
||||||
.extension {
|
|
||||||
padding: $padding $spacing;
|
|
||||||
background: $layoutBackground;
|
|
||||||
border-radius: $radius;
|
|
||||||
|
|
||||||
.actions > button:not(:last-child) {
|
|
||||||
margin-right: $spacing / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
h5, h6 {
|
|
||||||
color: var(--textColorSecondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .spinner-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.SearchInput {
|
|
||||||
--spacing: 10px;
|
|
||||||
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.InstallingExtensionNotification {
|
.InstallingExtensionNotification {
|
||||||
|
|||||||
@ -1,31 +1,48 @@
|
|||||||
import "./extensions.scss";
|
import "./extensions.scss";
|
||||||
|
|
||||||
import { remote, shell } from "electron";
|
import { remote, shell } from "electron";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
|
import _ from "lodash";
|
||||||
import { computed, observable, reaction, when } from "mobx";
|
import { computed, observable, reaction, when } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { autobind, disposer, Disposer, downloadFile, downloadJson, ExtendableDisposer, extractTar, listTarEntries, noop, readFileFromTar } from "../../../common/utils";
|
import { SemVer } from "semver";
|
||||||
|
import URLParse from "url-parse";
|
||||||
|
|
||||||
|
import {
|
||||||
|
autobind,
|
||||||
|
Disposer,
|
||||||
|
disposer,
|
||||||
|
downloadFile,
|
||||||
|
downloadJson,
|
||||||
|
ExtendableDisposer,
|
||||||
|
extractTar,
|
||||||
|
listTarEntries,
|
||||||
|
noop,
|
||||||
|
readFileFromTar,
|
||||||
|
} from "../../../common/utils";
|
||||||
import { docsUrl } from "../../../common/vars";
|
import { docsUrl } from "../../../common/vars";
|
||||||
import { ExtensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery";
|
import { ExtensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery";
|
||||||
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||||
import { extensionDisplayName, LensExtensionId, LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension";
|
import {
|
||||||
|
extensionDisplayName,
|
||||||
|
LensExtensionId,
|
||||||
|
LensExtensionManifest,
|
||||||
|
sanitizeExtensionName,
|
||||||
|
} from "../../../extensions/lens-extension";
|
||||||
import logger from "../../../main/logger";
|
import logger from "../../../main/logger";
|
||||||
import { prevDefault } from "../../utils";
|
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { ConfirmDialog } from "../confirm-dialog";
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { DropFileInput, Input, InputValidator, InputValidators, SearchInput } from "../input";
|
import { DropFileInput, InputValidators } from "../input";
|
||||||
import { PageLayout } from "../layout/page-layout";
|
import { PageLayout } from "../layout/page-layout";
|
||||||
import { SubTitle } from "../layout/sub-title";
|
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { Spinner } from "../spinner/spinner";
|
import { Spinner } from "../spinner/spinner";
|
||||||
import { TooltipPosition } from "../tooltip";
|
|
||||||
import { ExtensionInstallationState, ExtensionInstallationStateStore } from "./extension-install.store";
|
import { ExtensionInstallationState, ExtensionInstallationStateStore } from "./extension-install.store";
|
||||||
import URLParse from "url-parse";
|
import { Install } from "./install";
|
||||||
import { SemVer } from "semver";
|
import { InstalledExtensions } from "./installed-extensions";
|
||||||
import _ from "lodash";
|
|
||||||
|
|
||||||
function getMessageFromError(error: any): string {
|
function getMessageFromError(error: any): string {
|
||||||
if (!error || typeof error !== "object") {
|
if (!error || typeof error !== "object") {
|
||||||
@ -446,19 +463,6 @@ async function installFromSelectFileDialog() {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Extensions extends React.Component {
|
export class Extensions extends React.Component {
|
||||||
private static installInputValidators = [
|
|
||||||
InputValidators.isUrl,
|
|
||||||
InputValidators.isPath,
|
|
||||||
InputValidators.isExtensionNameInstall,
|
|
||||||
];
|
|
||||||
|
|
||||||
private static installInputValidator: InputValidator = {
|
|
||||||
message: "Invalid URL, absolute path, or extension name",
|
|
||||||
validate: (value: string) => (
|
|
||||||
Extensions.installInputValidators.some(({ validate }) => validate(value))
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
@observable search = "";
|
@observable search = "";
|
||||||
@observable installPath = "";
|
@observable installPath = "";
|
||||||
|
|
||||||
@ -566,62 +570,31 @@ export class Extensions extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { installPath } = this;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropFileInput onDropFiles={installOnDrop}>
|
<DropFileInput onDropFiles={installOnDrop}>
|
||||||
<PageLayout showOnTop className="Extensions" contentGaps={false}>
|
<PageLayout showOnTop className="Extensions" contentGaps={false}>
|
||||||
<h2>Lens Extensions</h2>
|
<section>
|
||||||
<div>
|
<h2>Lens Extensions</h2>
|
||||||
Add new features and functionality via Lens Extensions.
|
<div>
|
||||||
Check out documentation to <a href={`${docsUrl}/latest/extensions/usage/`} target="_blank" rel="noreferrer">learn more</a> or see the list of <a href="https://github.com/lensapp/lens-extensions/blob/main/README.md" target="_blank" rel="noreferrer">available extensions</a>.
|
Add new features and functionality via Lens Extensions.
|
||||||
</div>
|
Check out documentation to <a href={`${docsUrl}/latest/extensions/usage/`} target="_blank" rel="noreferrer">learn more</a> or see the list of <a href="https://github.com/lensapp/lens-extensions/blob/main/README.md" target="_blank" rel="noreferrer">available extensions</a>.
|
||||||
|
|
||||||
<div className="install-extension flex column gaps">
|
|
||||||
<SubTitle title="Install Extension:"/>
|
|
||||||
<div className="extension-input flex box gaps align-center">
|
|
||||||
<Input
|
|
||||||
className="box grow"
|
|
||||||
theme="round-black"
|
|
||||||
disabled={ExtensionInstallationStateStore.anyPreInstallingOrInstalling}
|
|
||||||
placeholder={`Name or file path or URL to an extension package (${supportedFormats.join(", ")})`}
|
|
||||||
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
|
||||||
validators={installPath ? Extensions.installInputValidator : undefined}
|
|
||||||
value={installPath}
|
|
||||||
onChange={value => this.installPath = value}
|
|
||||||
onSubmit={() => installFromInput(this.installPath)}
|
|
||||||
iconLeft="link"
|
|
||||||
iconRight={
|
|
||||||
<Icon
|
|
||||||
interactive
|
|
||||||
material="folder"
|
|
||||||
onClick={prevDefault(installFromSelectFileDialog)}
|
|
||||||
tooltip="Browse"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
label="Install"
|
|
||||||
disabled={ExtensionInstallationStateStore.anyPreInstallingOrInstalling || !Extensions.installInputValidator.validate(installPath)}
|
|
||||||
waiting={ExtensionInstallationStateStore.anyPreInstallingOrInstalling}
|
|
||||||
onClick={() => installFromInput(this.installPath)}
|
|
||||||
/>
|
|
||||||
<small className="hint">
|
|
||||||
<b>Pro-Tip</b>: you can also drag-n-drop tarball-file to this area
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Installed Extensions</h2>
|
<Install
|
||||||
<div className="installed-extensions flex column gaps">
|
supportedFormats={supportedFormats}
|
||||||
<SearchInput
|
onChange={(value) => this.installPath = value}
|
||||||
placeholder="Search installed extensions by name or description"
|
installFromInput={() => installFromInput(this.installPath)}
|
||||||
value={this.search}
|
installFromSelectFileDialog={installFromSelectFileDialog}
|
||||||
onChange={(value) => this.search = value}
|
installPath={this.installPath}
|
||||||
/>
|
/>
|
||||||
{this.renderExtensions()}
|
|
||||||
</div>
|
<hr/>
|
||||||
|
|
||||||
|
<InstalledExtensions
|
||||||
|
extensions={Array.from(ExtensionLoader.getInstance().userExtensions.values())}
|
||||||
|
uninstall={confirmUninstallExtension}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</DropFileInput>
|
</DropFileInput>
|
||||||
);
|
);
|
||||||
|
|||||||
76
src/renderer/components/+extensions/install.tsx
Normal file
76
src/renderer/components/+extensions/install.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { prevDefault } from "../../utils";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { Input, InputValidator, InputValidators } from "../input";
|
||||||
|
import { SubTitle } from "../layout/sub-title";
|
||||||
|
import { TooltipPosition } from "../tooltip";
|
||||||
|
import { ExtensionInstallationStateStore } from "./extension-install.store";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
installPath: string;
|
||||||
|
supportedFormats: string[];
|
||||||
|
onChange: (path: string) => void;
|
||||||
|
installFromInput: () => void;
|
||||||
|
installFromSelectFileDialog: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installInputValidators = [
|
||||||
|
InputValidators.isUrl,
|
||||||
|
InputValidators.isPath,
|
||||||
|
InputValidators.isExtensionNameInstall,
|
||||||
|
];
|
||||||
|
|
||||||
|
const installInputValidator: InputValidator = {
|
||||||
|
message: "Invalid URL, absolute path, or extension name",
|
||||||
|
validate: (value: string) => (
|
||||||
|
installInputValidators.some(({ validate }) => validate(value))
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Install(props: Props) {
|
||||||
|
const { installPath, supportedFormats, onChange, installFromInput, installFromSelectFileDialog } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h2>Install</h2>
|
||||||
|
<SubTitle title={`Path or URL to an extension package (${supportedFormats.join(", ")})`}/>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
className="box grow mr-6"
|
||||||
|
theme="round-black"
|
||||||
|
disabled={ExtensionInstallationStateStore.anyPreInstallingOrInstalling}
|
||||||
|
placeholder={"File path or URL"}
|
||||||
|
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
||||||
|
validators={installPath ? installInputValidator : undefined}
|
||||||
|
value={installPath}
|
||||||
|
onChange={onChange}
|
||||||
|
onSubmit={installFromInput}
|
||||||
|
iconRight={
|
||||||
|
<Icon
|
||||||
|
interactive
|
||||||
|
material="folder"
|
||||||
|
onClick={prevDefault(installFromSelectFileDialog)}
|
||||||
|
tooltip="Browse"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-initial">
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
label="Install"
|
||||||
|
className="w-80 h-full"
|
||||||
|
disabled={ExtensionInstallationStateStore.anyPreInstallingOrInstalling || !installInputValidator.validate(installPath)}
|
||||||
|
waiting={ExtensionInstallationStateStore.anyPreInstallingOrInstalling}
|
||||||
|
onClick={installFromInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small className="mt-1">
|
||||||
|
<b>Pro-Tip</b>: you can drag-n-drop tarball-file to this area
|
||||||
|
</small>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
.extensionName {
|
||||||
|
@apply font-bold;
|
||||||
|
color: var(--textColorAccent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.extensionDescription {
|
||||||
|
@apply mt-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enabled {
|
||||||
|
color: var(--colorOk);
|
||||||
|
}
|
||||||
132
src/renderer/components/+extensions/installed-extensions.tsx
Normal file
132
src/renderer/components/+extensions/installed-extensions.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import styles from "./installed-extensions.module.css";
|
||||||
|
import { Avatar } from "@material-ui/core";
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery";
|
||||||
|
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { List } from "../list/list";
|
||||||
|
import { MenuActions, MenuItem } from "../menu";
|
||||||
|
import { Spinner } from "../spinner";
|
||||||
|
import { ExtensionInstallationStateStore } from "./extension-install.store";
|
||||||
|
import { cssNames } from "../../utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
extensions: InstalledExtension[];
|
||||||
|
uninstall: (extension: InstalledExtension) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatus(isEnabled: boolean) {
|
||||||
|
return isEnabled ? "Enabled" : "Disabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InstalledExtensions({ extensions, uninstall }: Props) {
|
||||||
|
if (!ExtensionDiscovery.getInstance().isLoaded) {
|
||||||
|
return <div><Spinner /></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ExtensionLoader.getInstance().userExtensions.size == 0) {
|
||||||
|
// TODO: Add placeholder
|
||||||
|
return <div>No extensions</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = [
|
||||||
|
(extension: InstalledExtension) => extension.manifest.name,
|
||||||
|
(extension: InstalledExtension) => getStatus(extension.isEnabled),
|
||||||
|
(extension: InstalledExtension) => extension.manifest.version,
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
Header: "Extension",
|
||||||
|
accessor: "extension",
|
||||||
|
width: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "Version",
|
||||||
|
accessor: "version",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "Status",
|
||||||
|
accessor: "status"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "",
|
||||||
|
accessor: "actions",
|
||||||
|
disableSortBy: true,
|
||||||
|
width: 20,
|
||||||
|
className: "actions"
|
||||||
|
}
|
||||||
|
], []
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = useMemo(
|
||||||
|
() => {
|
||||||
|
return extensions.map(extension => {
|
||||||
|
const { id, isEnabled, manifest } = extension;
|
||||||
|
const { name, description, version } = manifest;
|
||||||
|
const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
extension: (
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="mr-4 mt-2">
|
||||||
|
<Avatar/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={styles.extensionName}>{name}</div>
|
||||||
|
<div className={styles.extensionDescription}>{description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
version,
|
||||||
|
status: (
|
||||||
|
<div className={cssNames({[styles.enabled]: getStatus(isEnabled) == "Enabled"})}>
|
||||||
|
{getStatus(isEnabled)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
actions: (
|
||||||
|
<MenuActions usePortal toolbar={false}>
|
||||||
|
{isEnabled ? (
|
||||||
|
<MenuItem
|
||||||
|
disabled={isUninstalling}
|
||||||
|
onClick={() => extension.isEnabled = false}
|
||||||
|
>
|
||||||
|
<Icon material="unpublished"/>
|
||||||
|
<span className="title">Disable</span>
|
||||||
|
</MenuItem>
|
||||||
|
) : (
|
||||||
|
<MenuItem
|
||||||
|
disabled={isUninstalling}
|
||||||
|
onClick={() => extension.isEnabled = true}
|
||||||
|
>
|
||||||
|
<Icon material="published"/>
|
||||||
|
<span className="title">Enable</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem
|
||||||
|
disabled={isUninstalling}
|
||||||
|
onClick={() => uninstall(extension)}
|
||||||
|
>
|
||||||
|
<Icon material="remove"/>
|
||||||
|
<span className="title">Uninstall</span>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuActions>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [extensions]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<List
|
||||||
|
title="Manage your extensions in here."
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
items={extensions}
|
||||||
|
filters={filters}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -90,6 +90,10 @@
|
|||||||
&.active {
|
&.active {
|
||||||
background-color: var(--navSelectedBackground);
|
background-color: var(--navSelectedBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -150,10 +154,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--colorInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -168,16 +168,19 @@
|
|||||||
|
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
color: var(--textColorAccent);
|
color: var(--textColorAccent);
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--colorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
8
src/renderer/components/list/list.module.css
Normal file
8
src/renderer/components/list/list.module.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.notFound {
|
||||||
|
@apply flex items-center justify-center h-10;
|
||||||
|
color: var(--textColorDimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput>label {
|
||||||
|
box-shadow: none!important;
|
||||||
|
}
|
||||||
44
src/renderer/components/list/list.tsx
Normal file
44
src/renderer/components/list/list.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import styles from "./list.module.css";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { SearchInput } from "../input";
|
||||||
|
|
||||||
|
import type { UseTableOptions } from "react-table";
|
||||||
|
import { Table } from "../table/react-table";
|
||||||
|
export type SearchFilter = (item: object) => string | number;
|
||||||
|
|
||||||
|
interface Props extends UseTableOptions<any> {
|
||||||
|
items: object[];
|
||||||
|
filters: SearchFilter[];
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function List({ columns, data, title, items, filters, }: Props) {
|
||||||
|
const [search, setSearch] = useState<string>("");
|
||||||
|
const query = search.toLowerCase();
|
||||||
|
|
||||||
|
const filteredData = data.filter((item, index) => (
|
||||||
|
filters.some(getText => String(getText(items[index])).toLowerCase().includes(query))
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex align-center justify-between mb-8">
|
||||||
|
<div className="mr-6">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<SearchInput
|
||||||
|
value={search}
|
||||||
|
theme="round-black"
|
||||||
|
onChange={setSearch}
|
||||||
|
className={styles.searchInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Table columns={columns} data={filteredData}/>
|
||||||
|
{filteredData.length == 0 && (
|
||||||
|
<div className={styles.notFound}>No data found</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user