Restyling extensions page with tailwindcss (#2796)

* Setting up tailwind and css modules env

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Using tailwind with scss files also

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Introducing react-table

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Spread extensions to smaller components

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add table sorting

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixing inputs line-height

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fine-tuning page view

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Align table rows

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding extension notice

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fine-tuning overall styling

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding a extensions placeholder

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Updating MaterialIcons font

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Aligning not found state

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Making extension components observable

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixing search input cross icon

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fix drag-n-drop indication

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixing extension name sorting

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fix linter

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixing tests

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Ignoring ts files to tailwind purge

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Cleaning up

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Renaming Table -> ReactTable

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixing integration tests

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Moving tailwind imports into app.scss

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Moving userExtensionList() out from extension-loader

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Transform extension list to array

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Expand install input placeholder a bit

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev authored and GitHub committed 2021-05-23 15:15:42 +03:00
1 parent ef21eba724
commit 0899ace037
35 files changed
+1677 -360

No files matched your search

@@ -88,9 +88,13 @@ describe("Extensions", () => {
ExtensionDiscovery.getInstance().isLoaded = true;
const res = render(<><Extensions /><ConfirmDialog /></>);
const table = res.getByTestId("extensions-table");
const menuTrigger = table.querySelector("div[role=row]:first-of-type .actions .Icon");
expect(res.getByText("Disable").closest("button")).not.toBeDisabled();
expect(res.getByText("Uninstall").closest("button")).not.toBeDisabled();
fireEvent.click(menuTrigger);
expect(res.getByText("Disable")).toHaveAttribute("aria-disabled", "false");
expect(res.getByText("Uninstall")).toHaveAttribute("aria-disabled", "false");
fireEvent.click(res.getByText("Uninstall"));
@@ -99,8 +103,9 @@ describe("Extensions", () => {
await waitFor(() => {
expect(ExtensionDiscovery.getInstance().uninstallExtension).toHaveBeenCalled();
expect(res.getByText("Disable").closest("button")).toBeDisabled();
expect(res.getByText("Uninstall").closest("button")).toBeDisabled();
fireEvent.click(menuTrigger);
expect(res.getByText("Disable")).toHaveAttribute("aria-disabled", "true");
expect(res.getByText("Uninstall")).toHaveAttribute("aria-disabled", "true");
}, {
timeout: 30000,
});
@@ -111,7 +116,7 @@ describe("Extensions", () => {
(fse.unlink as jest.MockedFunction<typeof fse.unlink>).mockReturnValue(Promise.resolve() as any);
fireEvent.change(res.getByPlaceholderText("Path or URL to an extension package", {
fireEvent.change(res.getByPlaceholderText("File path or URL", {
exact: false
}), {
target: {
@@ -134,8 +139,6 @@ describe("Extensions", () => {
ExtensionDiscovery.getInstance().isLoaded = true;
const { container } = render(<Extensions />);
waitFor(() =>
expect(container.querySelector(".Spinner")).not.toBeInTheDocument()
);
expect(container.querySelector(".Spinner")).not.toBeInTheDocument();
});
});
@@ -210,31 +210,45 @@ export class ExtensionInstallationStateStore {
return ExtensionInstallationStateStore.InstallingExtensions.size;
}
/**
* The current number of extensions uninstalling
*/
static get uninstalling(): number {
return ExtensionInstallationStateStore.UninstallingExtensions.size;
}
/**
* If there is at least one extension currently installing
*/
@computed static get anyInstalling(): boolean {
static get anyInstalling(): boolean {
return ExtensionInstallationStateStore.installing > 0;
}
/**
* If there is at least one extension currently ininstalling
*/
static get anyUninstalling(): boolean {
return ExtensionInstallationStateStore.uninstalling > 0;
}
/**
* The current number of extensions preinstalling
*/
@computed static get preinstalling(): number {
static get preinstalling(): number {
return ExtensionInstallationStateStore.PreInstallIds.size;
}
/**
* If there is at least one extension currently downloading
*/
@computed static get anyPreinstalling(): boolean {
static get anyPreinstalling(): boolean {
return ExtensionInstallationStateStore.preinstalling > 0;
}
/**
* If there is at least one installing or preinstalling step taking place
*/
@computed static get anyPreInstallingOrInstalling(): boolean {
* If there is at least one installing or preinstalling step taking place
*/
static get anyPreInstallingOrInstalling(): boolean {
return ExtensionInstallationStateStore.anyInstalling || ExtensionInstallationStateStore.anyPreinstalling;
}
}
@@ -21,52 +21,16 @@
.PageLayout.Extensions {
$spacing: $padding * 2;
--width: 50%;
width: 100%;
h2 {
margin-bottom: $padding;
}
.contentRegion {
.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);
> section {
height: 100%;
}
}
> .spinner-wrapper {
display: flex;
justify-content: center;
}
}
.SearchInput {
--spacing: 10px;
max-width: none;
}
}
@@ -20,33 +20,47 @@
*/
import "./extensions.scss";
import { remote, shell } from "electron";
import fse from "fs-extra";
import { computed, observable, reaction, when } from "mobx";
import _ from "lodash";
import { 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 { docsUrl } from "../../../common/vars";
import { SemVer } from "semver";
import URLParse from "url-parse";
import {
Disposer,
disposer,
downloadFile,
downloadJson,
ExtendableDisposer,
extractTar,
listTarEntries,
noop,
readFileFromTar,
} from "../../../common/utils";
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";
import { Notice } from "./notice";
function getMessageFromError(error: any): string {
if (!error || typeof error !== "object") {
@@ -89,6 +103,22 @@ interface InstallRequestValidated {
tempFile: string; // temp system path to packed extension for unpacking
}
function setExtensionEnabled(id: LensExtensionId, isEnabled: boolean): void {
const extension = ExtensionLoader.getInstance().getExtension(id);
if (extension) {
extension.isEnabled = isEnabled;
}
}
function enableExtension(id: LensExtensionId) {
setExtensionEnabled(id, true);
}
function disableExtension(id: LensExtensionId) {
setExtensionEnabled(id, false);
}
async function uninstallExtension(extensionId: LensExtensionId): Promise<boolean> {
const loader = ExtensionLoader.getInstance();
const { manifest } = loader.getExtension(extensionId);
@@ -465,32 +495,8 @@ 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 = "";
@computed get searchedForExtensions() {
const searchText = this.search.toLowerCase();
return Array.from(ExtensionLoader.getInstance().userExtensions.values())
.filter(({ manifest: { name, description }}) => (
name.toLowerCase().includes(searchText)
|| description?.toLowerCase().includes(searchText)
));
}
componentDidMount() {
// TODO: change this after upgrading to mobx6 as that versions' reactions have this functionality
let prevSize = ExtensionLoader.getInstance().userExtensions.size;
@@ -509,138 +515,34 @@ export class Extensions extends React.Component {
]);
}
renderNoExtensionsHelpText() {
if (this.search) {
return <p>No search results found</p>;
}
return (
<p>
There are no installed extensions.
See list of <a href="https://github.com/lensapp/lens-extensions/blob/main/README.md" target="_blank" rel="noreferrer">available extensions</a>.
</p>
);
}
renderNoExtensions() {
return (
<div className="no-extensions flex box gaps justify-center">
<Icon material="info" />
<div>
{this.renderNoExtensionsHelpText()}
</div>
</div>
);
}
@autobind()
renderExtension(extension: InstalledExtension) {
const { id, isEnabled, manifest } = extension;
const { name, description, version } = manifest;
const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id);
return (
<div key={id} className="extension flex gaps align-center">
<div className="box grow">
<h5>{name}</h5>
<h6>{version}</h6>
<p>{description}</p>
</div>
<div className="actions">
{
isEnabled
? <Button accent disabled={isUninstalling} onClick={() => extension.isEnabled = false}>Disable</Button>
: <Button plain active disabled={isUninstalling} onClick={() => extension.isEnabled = true}>Enable</Button>
}
<Button
plain
active
disabled={isUninstalling}
waiting={isUninstalling}
onClick={() => confirmUninstallExtension(extension)}
>
Uninstall
</Button>
</div>
</div>
);
}
renderExtensions() {
if (!ExtensionDiscovery.getInstance().isLoaded) {
return <div className="spinner-wrapper"><Spinner /></div>;
}
const { searchedForExtensions } = this;
if (!searchedForExtensions.length) {
return this.renderNoExtensions();
}
return (
<>
{...searchedForExtensions.map(this.renderExtension)}
</>
);
}
render() {
const { installPath } = this;
const extensions = Array.from(ExtensionLoader.getInstance().userExtensions.values());
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>
<section>
<h1>Extensions</h1>
<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>
<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>
<Notice/>
<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>
{extensions.length > 0 && <hr/>}
<InstalledExtensions
extensions={extensions}
enable={enableExtension}
disable={disableExtension}
uninstall={confirmUninstallExtension}
/>
</section>
</PageLayout>
</DropFileInput>
);
@@ -0,0 +1,7 @@
.icon {
@apply h-5 w-5 mr-3 cursor-pointer;
}
.icon:hover {
color: var(--textColorAccent);
}
@@ -0,0 +1,100 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import styles from "./install.module.css";
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";
import { observer } from "mobx-react";
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 const Install = observer((props: Props) => {
const { installPath, supportedFormats, onChange, installFromInput, installFromSelectFileDialog } = props;
return (
<section className="mt-2">
<SubTitle title={`Name or file 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={"Name or file path or URL"}
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
validators={installPath ? installInputValidator : undefined}
value={installPath}
onChange={onChange}
onSubmit={installFromInput}
iconRight={
<Icon
interactive={false}
className={styles.icon}
material="folder_open"
onClick={prevDefault(installFromSelectFileDialog)}
tooltip="Browse"
/>
}
/>
</div>
<div className="flex-initial">
<Button
primary
label="Install"
className="w-80 h-full"
disabled={ExtensionInstallationStateStore.anyPreInstallingOrInstalling}
waiting={ExtensionInstallationStateStore.anyPreInstallingOrInstalling}
onClick={installFromInput}
/>
</div>
</div>
<small className="mt-3">
<b>Pro-Tip</b>: you can drag-n-drop tarball-file to this area
</small>
</section>
);
});
@@ -0,0 +1,25 @@
.extensionName {
@apply font-bold;
color: var(--textColorAccent);
}
.extensionDescription {
@apply mt-1;
}
.enabled {
color: var(--colorOk);
}
.title {
margin-bottom: 0!important;
}
.noItemsIcon {
@apply opacity-10;
--size: 180px;
}
.frozenRow {
@apply opacity-30 pointer-events-none;
}
@@ -0,0 +1,169 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import styles from "./installed-extensions.module.css";
import React, { useMemo } from "react";
import { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery";
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";
import { observer } from "mobx-react";
import type { Row } from "react-table";
import type { LensExtensionId } from "../../../extensions/lens-extension";
interface Props {
extensions: InstalledExtension[];
enable: (id: LensExtensionId) => void;
disable: (id: LensExtensionId) => void;
uninstall: (extension: InstalledExtension) => void;
}
function getStatus(isEnabled: boolean) {
return isEnabled ? "Enabled" : "Disabled";
}
export const InstalledExtensions = observer(({ extensions, uninstall, enable, disable }: Props) => {
const filters = [
(extension: InstalledExtension) => extension.manifest.name,
(extension: InstalledExtension) => getStatus(extension.isEnabled),
(extension: InstalledExtension) => extension.manifest.version,
];
const columns = useMemo(
() => [
{
Header: "Name",
accessor: "extension",
width: 200,
sortType: (rowA: Row, rowB: Row) => { // Custom sorting for extension name
const nameA = extensions[rowA.index].manifest.name;
const nameB = extensions[rowB.index].manifest.name;
if (nameA > nameB) return -1;
if (nameB > nameA) return 1;
return 0;
},
},
{
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>
<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={() => disable(id)}
>
<Icon material="unpublished"/>
<span className="title" aria-disabled={isUninstalling}>Disable</span>
</MenuItem>
) : (
<MenuItem
disabled={isUninstalling}
onClick={() => enable(id)}
>
<Icon material="check_circle"/>
<span className="title" aria-disabled={isUninstalling}>Enable</span>
</MenuItem>
)}
<MenuItem
disabled={isUninstalling}
onClick={() => uninstall(extension)}
>
<Icon material="delete"/>
<span className="title" aria-disabled={isUninstalling}>Uninstall</span>
</MenuItem>
</MenuActions>
)
};
});
}, [extensions, ExtensionInstallationStateStore.anyUninstalling]
);
if (!ExtensionDiscovery.getInstance().isLoaded) {
return <div><Spinner center /></div>;
}
if (extensions.length == 0) {
return (
<div className="flex column h-full items-center justify-center">
<Icon material="extension" className={styles.noItemsIcon}/>
<h3 className="font-medium text-3xl mt-5 mb-2">
There are no extensions installed.
</h3>
<p>Please use the form above to install or drag tarbar-file here.</p>
</div>
);
}
return (
<section data-testid="extensions-table">
<List
title={<h2 className={styles.title}>Installed extensions</h2>}
columns={columns}
data={data}
items={extensions}
filters={filters}
/>
</section>
);
});
@@ -0,0 +1,27 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.notice {
@apply p-8 flex flex-col gap-2 mb-14 mt-3 rounded-lg;
background-color: var(--inputControlBackground);
border: 1px solid var(--boxShadow);
color: var(--textColorTertiary);
}
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import styles from "./notice.module.css";
import React from "react";
import { docsUrl } from "../../../common/vars";
export function Notice() {
return (
<div className={styles.notice}>
<p>
Add new features via Lens Extensions.{" "}
Check out <a href={`${docsUrl}/extensions/`} target="_blank" rel="noreferrer">docs</a>{" "}
and list of <a href="https://github.com/lensapp/lens-extensions/blob/main/README.md" target="_blank" rel="noreferrer">available extensions</a>.
</p>
</div>
);
}