diff --git a/src/renderer/components/+extensions/extensions.scss b/src/renderer/components/+extensions/extensions.scss index 86f9d7b232..21acbceed9 100644 --- a/src/renderer/components/+extensions/extensions.scss +++ b/src/renderer/components/+extensions/extensions.scss @@ -1,52 +1,12 @@ .PageLayout.Extensions { $spacing: $padding * 2; - --width: 50%; + width: 100%; - h2 { - margin-bottom: $padding; - } - - .no-extensions { - --flex-gap: #{$padding}; - padding: $padding; - - code { - font-size: $font-size-small; + .contentRegion { + .content { + max-width: 740px; } } - - .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 { diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 968f16c63e..ca2fa30f94 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -1,31 +1,48 @@ import "./extensions.scss"; + import { remote, shell } from "electron"; import fse from "fs-extra"; +import _ from "lodash"; import { computed, observable, reaction, when } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import os from "os"; import path from "path"; 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 { ExtensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery"; 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 { prevDefault } from "../../utils"; import { Button } from "../button"; import { ConfirmDialog } from "../confirm-dialog"; import { Icon } from "../icon"; -import { DropFileInput, Input, InputValidator, InputValidators, SearchInput } from "../input"; +import { DropFileInput, InputValidators } from "../input"; import { PageLayout } from "../layout/page-layout"; -import { SubTitle } from "../layout/sub-title"; import { Notifications } from "../notifications"; import { Spinner } from "../spinner/spinner"; -import { TooltipPosition } from "../tooltip"; import { ExtensionInstallationState, ExtensionInstallationStateStore } from "./extension-install.store"; -import URLParse from "url-parse"; -import { SemVer } from "semver"; -import _ from "lodash"; +import { Install } from "./install"; +import { InstalledExtensions } from "./installed-extensions"; function getMessageFromError(error: any): string { if (!error || typeof error !== "object") { @@ -446,19 +463,6 @@ async function installFromSelectFileDialog() { @observer 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 installPath = ""; @@ -566,62 +570,31 @@ export class Extensions extends React.Component { } render() { - const { installPath } = this; - return ( -

Lens Extensions

-
- Add new features and functionality via Lens Extensions. - Check out documentation to learn more or see the list of available extensions. -
- -
- -
- this.installPath = value} - onSubmit={() => installFromInput(this.installPath)} - iconLeft="link" - iconRight={ - - } - /> +
+

Lens Extensions

+
+ Add new features and functionality via Lens Extensions. + Check out documentation to learn more or see the list of available extensions.
-
-

Installed Extensions

-
- this.search = value} + this.installPath = value} + installFromInput={() => installFromInput(this.installPath)} + installFromSelectFileDialog={installFromSelectFileDialog} + installPath={this.installPath} /> - {this.renderExtensions()} -
+ +
+ + + ); diff --git a/src/renderer/components/+extensions/install.tsx b/src/renderer/components/+extensions/install.tsx new file mode 100644 index 0000000000..943fe6a9dc --- /dev/null +++ b/src/renderer/components/+extensions/install.tsx @@ -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 ( +
+

Install

+ +
+
+ + } + /> +
+
+
+
+ + Pro-Tip: you can drag-n-drop tarball-file to this area + +
+ ); +} diff --git a/src/renderer/components/+extensions/installed-extensions.module.css b/src/renderer/components/+extensions/installed-extensions.module.css new file mode 100644 index 0000000000..820849b0d5 --- /dev/null +++ b/src/renderer/components/+extensions/installed-extensions.module.css @@ -0,0 +1,12 @@ +.extensionName { + @apply font-bold; + color: var(--textColorAccent); +} + +.extensionDescription { + @apply mt-2; +} + +.enabled { + color: var(--colorOk); +} diff --git a/src/renderer/components/+extensions/installed-extensions.tsx b/src/renderer/components/+extensions/installed-extensions.tsx new file mode 100644 index 0000000000..bbf27bd76f --- /dev/null +++ b/src/renderer/components/+extensions/installed-extensions.tsx @@ -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
; + } + + if (ExtensionLoader.getInstance().userExtensions.size == 0) { + // TODO: Add placeholder + return
No extensions
; + } + + 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: ( +
+
+ +
+
+
{name}
+
{description}
+
+
+ ), + version, + status: ( +
+ {getStatus(isEnabled)} +
+ ), + actions: ( + + {isEnabled ? ( + extension.isEnabled = false} + > + + Disable + + ) : ( + extension.isEnabled = true} + > + + Enable + + )} + uninstall(extension)} + > + + Uninstall + + + ) + }; + }); + }, [extensions] + ); + + return ( +
+ +
+ ); +} diff --git a/src/renderer/components/layout/page-layout.scss b/src/renderer/components/layout/page-layout.scss index 2a4400d76f..29696c775e 100644 --- a/src/renderer/components/layout/page-layout.scss +++ b/src/renderer/components/layout/page-layout.scss @@ -90,6 +90,10 @@ &.active { background-color: var(--navSelectedBackground); } + + > .label { + width: 100%; + } } } } @@ -150,10 +154,6 @@ } } - a { - color: var(--colorInfo); - } - section { display: flex; flex-direction: column; @@ -168,16 +168,19 @@ h1, h2 { color: var(--textColorAccent); - text-transform: uppercase; } h2 { - font-size: 16px; + font-size: 18px; line-height: 20px; font-weight: 600; margin-bottom: 20px; } + a { + color: var(--colorInfo); + } + .hint { margin-top: 8px; font-size: 14px; diff --git a/src/renderer/components/list/list.module.css b/src/renderer/components/list/list.module.css new file mode 100644 index 0000000000..c64c86620a --- /dev/null +++ b/src/renderer/components/list/list.module.css @@ -0,0 +1,8 @@ +.notFound { + @apply flex items-center justify-center h-10; + color: var(--textColorDimmed); +} + +.searchInput>label { + box-shadow: none!important; +} \ No newline at end of file diff --git a/src/renderer/components/list/list.tsx b/src/renderer/components/list/list.tsx new file mode 100644 index 0000000000..6fae6036c7 --- /dev/null +++ b/src/renderer/components/list/list.tsx @@ -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 { + items: object[]; + filters: SearchFilter[]; + title?: string; +} + +export function List({ columns, data, title, items, filters, }: Props) { + const [search, setSearch] = useState(""); + const query = search.toLowerCase(); + + const filteredData = data.filter((item, index) => ( + filters.some(getText => String(getText(items[index])).toLowerCase().includes(query)) + )); + + return ( + <> +
+
+ {title} +
+
+ +
+
+ + {filteredData.length == 0 && ( +
No data found
+ )} + + ); +}