diff --git a/extensions/metrics-cluster-feature/resources/03-statefulset.yml.hb b/extensions/metrics-cluster-feature/resources/03-statefulset.yml.hb index ee68619a30..cc177204a3 100644 --- a/extensions/metrics-cluster-feature/resources/03-statefulset.yml.hb +++ b/extensions/metrics-cluster-feature/resources/03-statefulset.yml.hb @@ -54,7 +54,7 @@ spec: mountPath: /var/lib/prometheus containers: - name: prometheus - image: quay.io/prometheus/prometheus:v2.26.0 + image: quay.io/prometheus/prometheus:v2.27.1 args: - --web.listen-address=0.0.0.0:9090 - --config.file=/etc/prometheus/prometheus.yaml diff --git a/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb b/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb index c895c9831c..5eaefe2cf9 100644 --- a/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb +++ b/extensions/metrics-cluster-feature/resources/14-kube-state-metrics-deployment.yml.hb @@ -23,23 +23,15 @@ spec: operator: In values: - linux - - key: kubernetes.io/arch - operator: In - values: - - amd64 - matchExpressions: - key: beta.kubernetes.io/os operator: In values: - linux - - key: beta.kubernetes.io/arch - operator: In - values: - - amd64 serviceAccountName: kube-state-metrics containers: - name: kube-state-metrics - image: quay.io/coreos/kube-state-metrics:v1.9.8 + image: k8s.gcr.io/kube-state-metrics/kube-state-metrics:v2.0.0 ports: - name: metrics containerPort: 8080 diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index d433513f1e..7b8ff11435 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -57,17 +57,17 @@ describe("Lens integration tests", () => { const appName: string = process.platform === "darwin" ? "OpenLens" : "File"; await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences"); - await app.client.waitUntilTextExists("[data-testid=application-header]", "APPLICATION"); + await app.client.waitUntilTextExists("[data-testid=application-header]", "Application"); }); it("shows all tabs and their contents", async () => { await app.client.click("[data-testid=application-tab]"); await app.client.click("[data-testid=proxy-tab]"); - await app.client.waitUntilTextExists("[data-testid=proxy-header]", "PROXY"); + await app.client.waitUntilTextExists("[data-testid=proxy-header]", "Proxy"); await app.client.click("[data-testid=kube-tab]"); - await app.client.waitUntilTextExists("[data-testid=kubernetes-header]", "KUBERNETES"); + await app.client.waitUntilTextExists("[data-testid=kubernetes-header]", "Kubernetes"); await app.client.click("[data-testid=telemetry-tab]"); - await app.client.waitUntilTextExists("[data-testid=telemetry-header]", "TELEMETRY"); + await app.client.waitUntilTextExists("[data-testid=telemetry-header]", "Telemetry"); }); it("ensures helm repos", async () => { diff --git a/package.json b/package.json index 02b2c494e9..17bcc79742 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "open-lens", "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", - "version": "5.0.0-beta.5", + "version": "5.0.0-beta.6", "main": "static/build/main.js", "copyright": "© 2021 OpenLens Authors", "license": "MIT", @@ -241,7 +241,7 @@ }, "devDependencies": { "@emeraldpay/hashicon-react": "^0.4.0", - "@material-ui/core": "^4.10.1", + "@material-ui/core": "^4.11.4", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.57", "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", @@ -280,6 +280,7 @@ "@types/react-dom": "^17.0.0", "@types/react-router-dom": "^5.1.6", "@types/react-select": "^3.0.13", + "@types/react-table": "^7.7.0", "@types/react-window": "^1.8.2", "@types/readable-stream": "^2.3.9", "@types/request": "^2.48.5", @@ -336,6 +337,8 @@ "nodemon": "^2.0.4", "open": "^7.3.1", "patch-package": "^6.2.2", + "postcss": "^8.2.14", + "postcss-loader": "~3.0.0", "postinstall-postinstall": "^2.1.0", "prettier": "^2.2.0", "progress-bar-webpack-plugin": "^2.1.0", @@ -346,11 +349,13 @@ "react-router-dom": "^5.2.0", "react-select": "^3.1.0", "react-select-event": "^5.1.0", + "react-table": "^7.7.0", "react-window": "^1.8.5", "sass-loader": "^8.0.2", "sharp": "^0.26.1", "spectron": "11.0.0", "style-loader": "^1.2.1", + "tailwindcss": "^2.1.2", "ts-jest": "26.3.0", "ts-loader": "^7.0.5", "ts-node": "^8.10.2", @@ -359,6 +364,7 @@ "typedoc-plugin-markdown": "^2.4.0", "typeface-roboto": "^0.0.75", "typescript": "4.0.2", + "typescript-plugin-css-modules": "^3.2.0", "url-loader": "^4.1.0", "webpack": "^4.44.2", "webpack-cli": "^3.3.11", diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000..2ae5d843b6 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,28 @@ +/** + * 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. + */ + +const tailwindcss = require("tailwindcss"); + +module.exports = { + plugins: [ + tailwindcss("./tailwind.config.js") + ], +}; diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index b024ddd761..923093c595 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -61,7 +61,7 @@ export class ExtensionLoader extends Singleton { whenLoaded = when(() => this.isLoaded); @computed get userExtensions(): Map { - const extensions = this.extensions.toJS(); + const extensions = this.toJSON(); extensions.forEach((ext, extId) => { if (ext.isBundled) { diff --git a/src/renderer/components/+catalog/catalog.scss b/src/renderer/components/+catalog/catalog.scss index 5f8131dfaa..e76606567b 100644 --- a/src/renderer/components/+catalog/catalog.scss +++ b/src/renderer/components/+catalog/catalog.scss @@ -69,7 +69,18 @@ } } + .TableCell.icon { + max-width: 40px; + display: flex; + align-items: center; + } + + .TableCell.source { + max-width: 100px; + } + .TableCell.status { + max-width: 100px; &.connected { color: var(--colorSuccess); } @@ -97,4 +108,9 @@ } } } + + .catalogIcon { + font-size: 10px; + -webkit-font-smoothing: auto; + } } diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 5211e034c2..7f5d10b015 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -40,6 +40,7 @@ import { CatalogAddButton } from "./catalog-add-button"; import type { RouteComponentProps } from "react-router"; import type { ICatalogViewRouteParam } from "./catalog.route"; import { Notifications } from "../notifications"; +import { Avatar } from "../avatar/avatar"; enum sortBy { name = "name", @@ -174,6 +175,24 @@ export class Catalog extends React.Component { ); } + renderIcon(item: CatalogEntityItem) { + const category = catalogCategoryRegistry.getCategoryForEntity(item.entity); + + if (!category) { + return null; + } + + return ( + + ); + } + render() { if (!this.catalogEntityStore) { return null; @@ -202,12 +221,14 @@ export class Catalog extends React.Component { (entity: CatalogEntityItem) => entity.searchFields, ]} renderTableHeader={[ + { title: "", className: "icon" }, { title: "Name", className: "name", sortBy: sortBy.name }, { title: "Source", className: "source", sortBy: sortBy.source }, { title: "Labels", className: "labels" }, { title: "Status", className: "status", sortBy: sortBy.status }, ]} renderTableContents={(item: CatalogEntityItem) => [ + this.renderIcon(item), item.name, item.source, item.labels.map((label) => ), diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 5aee03bb6e..40a523d8a3 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -88,9 +88,13 @@ describe("Extensions", () => { ExtensionDiscovery.getInstance().isLoaded = true; const res = render(<>); + 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).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(); - waitFor(() => - expect(container.querySelector(".Spinner")).not.toBeInTheDocument() - ); + expect(container.querySelector(".Spinner")).not.toBeInTheDocument(); }); }); diff --git a/src/renderer/components/+extensions/extension-install.store.ts b/src/renderer/components/+extensions/extension-install.store.ts index 79ba16fa01..c02bb482fd 100644 --- a/src/renderer/components/+extensions/extension-install.store.ts +++ b/src/renderer/components/+extensions/extension-install.store.ts @@ -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; } } diff --git a/src/renderer/components/+extensions/extensions.scss b/src/renderer/components/+extensions/extensions.scss index 13b9be1f2f..0369eb4798 100644 --- a/src/renderer/components/+extensions/extensions.scss +++ b/src/renderer/components/+extensions/extensions.scss @@ -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; } } diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 93923d3093..e050475fce 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -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 { 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

No search results found

; - } - - return ( -

- There are no installed extensions. - See list of available extensions. -

- ); - } - - renderNoExtensions() { - return ( -
- -
- {this.renderNoExtensionsHelpText()} -
-
- ); - } - - @autobind() - renderExtension(extension: InstalledExtension) { - const { id, isEnabled, manifest } = extension; - const { name, description, version } = manifest; - const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id); - - return ( -
-
-
{name}
-
{version}
-

{description}

-
-
- { - isEnabled - ? - : - } - -
-
- ); - } - - renderExtensions() { - if (!ExtensionDiscovery.getInstance().isLoaded) { - return
; - } - - 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 ( -

Lens Extensions

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

Extensions

-
- -
- this.installPath = value} - onSubmit={() => installFromInput(this.installPath)} - iconLeft="link" - iconRight={ - - } - /> -
-
+ -

Installed Extensions

-
- this.search = value} + this.installPath = value} + installFromInput={() => installFromInput(this.installPath)} + installFromSelectFileDialog={installFromSelectFileDialog} + installPath={this.installPath} /> - {this.renderExtensions()} -
+ + {extensions.length > 0 &&
} + + +
); diff --git a/src/renderer/components/+extensions/install.module.css b/src/renderer/components/+extensions/install.module.css new file mode 100644 index 0000000000..1cb5b5175a --- /dev/null +++ b/src/renderer/components/+extensions/install.module.css @@ -0,0 +1,7 @@ +.icon { + @apply h-5 w-5 mr-3 cursor-pointer; +} + +.icon:hover { + color: var(--textColorAccent); +} \ No newline at end of file diff --git a/src/renderer/components/+extensions/install.tsx b/src/renderer/components/+extensions/install.tsx new file mode 100644 index 0000000000..d07533d3ba --- /dev/null +++ b/src/renderer/components/+extensions/install.tsx @@ -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 ( +
+ +
+
+ + } + /> +
+
+
+
+ + 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..831ece9024 --- /dev/null +++ b/src/renderer/components/+extensions/installed-extensions.module.css @@ -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; +} \ No newline at end of file diff --git a/src/renderer/components/+extensions/installed-extensions.tsx b/src/renderer/components/+extensions/installed-extensions.tsx new file mode 100644 index 0000000000..f896f410fd --- /dev/null +++ b/src/renderer/components/+extensions/installed-extensions.tsx @@ -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: ( +
+
+
{name}
+
{description}
+
+
+ ), + version, + status: ( +
+ {getStatus(isEnabled)} +
+ ), + actions: ( + + {isEnabled ? ( + disable(id)} + > + + Disable + + ) : ( + enable(id)} + > + + Enable + + )} + uninstall(extension)} + > + + Uninstall + + + ) + }; + }); + }, [extensions, ExtensionInstallationStateStore.anyUninstalling] + ); + + if (!ExtensionDiscovery.getInstance().isLoaded) { + return
; + } + + if (extensions.length == 0) { + return ( +
+ +

+ There are no extensions installed. +

+

Please use the form above to install or drag tarbar-file here.

+
+ ); + } + + return ( +
+ Installed extensions} + columns={columns} + data={data} + items={extensions} + filters={filters} + /> +
+ ); +}); diff --git a/src/renderer/components/+extensions/notice.module.css b/src/renderer/components/+extensions/notice.module.css new file mode 100644 index 0000000000..81b90c48ad --- /dev/null +++ b/src/renderer/components/+extensions/notice.module.css @@ -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); +} \ No newline at end of file diff --git a/src/renderer/components/+extensions/notice.tsx b/src/renderer/components/+extensions/notice.tsx new file mode 100644 index 0000000000..d224b65a7d --- /dev/null +++ b/src/renderer/components/+extensions/notice.tsx @@ -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 ( +
+

+ Add new features via Lens Extensions.{" "} + Check out docs{" "} + and list of available extensions. +

+
+ ); +} diff --git a/src/renderer/components/+namespaces/namespace-select-filter.scss b/src/renderer/components/+namespaces/namespace-select-filter.scss new file mode 100644 index 0000000000..da59651ce5 --- /dev/null +++ b/src/renderer/components/+namespaces/namespace-select-filter.scss @@ -0,0 +1,47 @@ +.NamespaceSelectFilter { + --gradientColor: var(--select-menu-bgc); + + .Select { + &.theme-light { + --gradientColor: white; + } + + &__placeholder { + width: 100%; + white-space: nowrap; + overflow: scroll!important; + text-overflow: unset!important; + margin-left: -8px; + padding-left: 8px; + margin-right: -8px; + padding-right: 8px; + + &::-webkit-scrollbar { + display: none; + } + } + + &__value-container { + position: relative; + + &::before, &::after { + content: ' '; + position: absolute; + z-index: 20; + display: block; + width: 8px; + height: var(--font-size); + } + + &::before { + left: 0px; + background: linear-gradient(to right, var(--gradientColor) 0px, transparent); + } + + &::after { + right: 0px; + background: linear-gradient(to left, var(--gradientColor) 0px, transparent); + } + } + } +} diff --git a/src/renderer/components/+namespaces/namespace-select-filter.tsx b/src/renderer/components/+namespaces/namespace-select-filter.tsx index 8f4e9df6da..ad2c1fe87b 100644 --- a/src/renderer/components/+namespaces/namespace-select-filter.tsx +++ b/src/renderer/components/+namespaces/namespace-select-filter.tsx @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import "./namespace-select.scss"; +import "./namespace-select-filter.scss"; import React from "react"; import { observer } from "mobx-react"; @@ -28,10 +28,11 @@ import { components, PlaceholderProps } from "react-select"; import { Icon } from "../icon"; import { FilterIcon } from "../item-object-list/filter-icon"; import { FilterType } from "../item-object-list/page-filters.store"; -import type { SelectOption } from "../select"; import { NamespaceSelect } from "./namespace-select"; import { namespaceStore } from "./namespace.store"; +import type { SelectOption, SelectProps } from "../select"; + const Placeholder = observer((props: PlaceholderProps) => { const getPlaceholder = (): React.ReactNode => { const namespaces = namespaceStore.contextNamespaces; @@ -54,9 +55,8 @@ const Placeholder = observer((props: PlaceholderProps) => { ); }); - @observer -export class NamespaceSelectFilter extends React.Component { +export class NamespaceSelectFilter extends React.Component { formatOptionLabel({ value: namespace, label }: SelectOption) { if (namespace) { const isSelected = namespaceStore.hasContext(namespace); @@ -92,6 +92,7 @@ export class NamespaceSelectFilter extends React.Component { placeholder={""} onChange={this.onChange} formatOptionLabel={this.formatOptionLabel} + className="NamespaceSelectFilter" /> ); } diff --git a/src/renderer/components/+namespaces/namespace-select.scss b/src/renderer/components/+namespaces/namespace-select.scss index 72df94a9a7..e348f7025b 100644 --- a/src/renderer/components/+namespaces/namespace-select.scss +++ b/src/renderer/components/+namespaces/namespace-select.scss @@ -25,41 +25,8 @@ } } -.GradientValueContainer { - width: 8px; - height: var(--font-size); - position: absolute; - z-index: 20; - - &.front { - left: 0px; - background: linear-gradient(to right, var(--contentColor) 0px, transparent); - } - - &.back { - right: 0px; - background: linear-gradient(to left, var(--contentColor) 0px, transparent); - } -} - .NamespaceSelect { @include namespaceSelectCommon; - - .Select { - &__placeholder { - width: 100%; - white-space: nowrap; - overflow: scroll; - margin-left: -8px; - padding-left: 8px; - margin-right: -8px; - padding-right: 8px; - - &::-webkit-scrollbar { - display: none; - } - } - } } .NamespaceSelectMenu { diff --git a/src/renderer/components/+namespaces/namespace-select.tsx b/src/renderer/components/+namespaces/namespace-select.tsx index 3a17e791db..7c98323b74 100644 --- a/src/renderer/components/+namespaces/namespace-select.tsx +++ b/src/renderer/components/+namespaces/namespace-select.tsx @@ -29,7 +29,6 @@ import { cssNames } from "../../utils"; import { Icon } from "../icon"; import { namespaceStore } from "./namespace.store"; import { kubeWatchApi } from "../../api/kube-watch-api"; -import { components, ValueContainerProps } from "react-select"; interface Props extends SelectProps { showIcons?: boolean; @@ -43,16 +42,6 @@ const defaultProps: Partial = { showClusterOption: false, }; -function GradientValueContainer({children, ...rest}: ValueContainerProps) { - return ( - -
- {children} -
- - ); -} - @observer export class NamespaceSelect extends React.Component { static defaultProps = defaultProps as object; @@ -98,8 +87,6 @@ export class NamespaceSelect extends React.Component { render() { const { className, showIcons, customizeOptions, components = {}, ...selectProps } = this.props; - components.ValueContainer ??= GradientValueContainer; - return (