1
0
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:
Alex Andreev 2021-05-14 11:20:50 +03:00
parent 34bce1764d
commit 38343af2ef
8 changed files with 330 additions and 122 deletions

View File

@ -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 {

View File

@ -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 (
<DropFileInput onDropFiles={installOnDrop}>
<PageLayout showOnTop className="Extensions" contentGaps={false}>
<h2>Lens Extensions</h2>
<div>
Add new features and functionality via Lens Extensions.
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>
<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"
/>
}
/>
<section>
<h2>Lens Extensions</h2>
<div>
Add new features and functionality via Lens Extensions.
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>
<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>
<div className="installed-extensions flex column gaps">
<SearchInput
placeholder="Search installed extensions by name or description"
value={this.search}
onChange={(value) => this.search = value}
<Install
supportedFormats={supportedFormats}
onChange={(value) => this.installPath = value}
installFromInput={() => installFromInput(this.installPath)}
installFromSelectFileDialog={installFromSelectFileDialog}
installPath={this.installPath}
/>
{this.renderExtensions()}
</div>
<hr/>
<InstalledExtensions
extensions={Array.from(ExtensionLoader.getInstance().userExtensions.values())}
uninstall={confirmUninstallExtension}
/>
</section>
</PageLayout>
</DropFileInput>
);

View 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>
);
}

View File

@ -0,0 +1,12 @@
.extensionName {
@apply font-bold;
color: var(--textColorAccent);
}
.extensionDescription {
@apply mt-2;
}
.enabled {
color: var(--colorOk);
}

View 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>
);
}

View File

@ -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;

View File

@ -0,0 +1,8 @@
.notFound {
@apply flex items-center justify-center h-10;
color: var(--textColorDimmed);
}
.searchInput>label {
box-shadow: none!important;
}

View 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>
)}
</>
);
}