1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into back-to-catalog-button

This commit is contained in:
Alex Andreev 2021-05-25 10:02:45 +03:00
commit 54f74d453d
47 changed files with 1959 additions and 575 deletions

View File

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

View File

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

View File

@ -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 () => {

View File

@ -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",

28
postcss.config.js Normal file
View File

@ -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")
],
};

View File

@ -61,7 +61,7 @@ export class ExtensionLoader extends Singleton {
whenLoaded = when(() => this.isLoaded);
@computed get userExtensions(): Map<LensExtensionId, InstalledExtension> {
const extensions = this.extensions.toJS();
const extensions = this.toJSON();
extensions.forEach((ext, extId) => {
if (ext.isBundled) {

View File

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

View File

@ -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<Props> {
);
}
renderIcon(item: CatalogEntityItem) {
const category = catalogCategoryRegistry.getCategoryForEntity(item.entity);
if (!category) {
return null;
}
return (
<Avatar
title={item.name}
colorHash={`${item.name}-${item.source}`}
width={24}
height={24}
className="catalogIcon"
/>
);
}
render() {
if (!this.catalogEntityStore) {
return null;
@ -202,12 +221,14 @@ export class Catalog extends React.Component<Props> {
(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) => <Badge key={label} label={label} title={label} />),

View File

@ -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();
});
});

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
.icon {
@apply h-5 w-5 mr-3 cursor-pointer;
}
.icon:hover {
color: var(--textColorAccent);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<any>) => {
const getPlaceholder = (): React.ReactNode => {
const namespaces = namespaceStore.contextNamespaces;
@ -54,9 +55,8 @@ const Placeholder = observer((props: PlaceholderProps<any>) => {
);
});
@observer
export class NamespaceSelectFilter extends React.Component {
export class NamespaceSelectFilter extends React.Component<SelectProps> {
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"
/>
);
}

View File

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

View File

@ -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<Props> = {
showClusterOption: false,
};
function GradientValueContainer<T>({children, ...rest}: ValueContainerProps<T>) {
return (
<components.ValueContainer {...rest}>
<div className="GradientValueContainer front" />
{children}
<div className="GradientValueContainer back" />
</components.ValueContainer>
);
}
@observer
export class NamespaceSelect extends React.Component<Props> {
static defaultProps = defaultProps as object;
@ -98,8 +87,6 @@ export class NamespaceSelect extends React.Component<Props> {
render() {
const { className, showIcons, customizeOptions, components = {}, ...selectProps } = this.props;
components.ValueContainer ??= GradientValueContainer;
return (
<Select
className={cssNames("NamespaceSelect", className)}

View File

@ -19,6 +19,9 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "~flex.box";
@import "fonts";

View File

@ -18,7 +18,6 @@
* 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 React from "react";
import { observable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";

View File

@ -0,0 +1,92 @@
/**
* 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 React, { DOMAttributes } from "react";
import randomColor from "randomcolor";
import GraphemeSplitter from "grapheme-splitter";
import { Avatar as MaterialAvatar, AvatarTypeMap } from "@material-ui/core";
import { iter } from "../../utils";
interface Props extends DOMAttributes<HTMLElement>, Partial<AvatarTypeMap> {
title: string;
colorHash?: string;
width?: number;
height?: number;
src?: string;
className?: string;
}
function getNameParts(name: string): string[] {
const byWhitespace = name.split(/\s+/);
if (byWhitespace.length > 1) {
return byWhitespace;
}
const byDashes = name.split(/[-_]+/);
if (byDashes.length > 1) {
return byDashes;
}
return name.split(/@+/);
}
function getIconString(title: string) {
if (!title) {
return "??";
}
const [rawFirst, rawSecond, rawThird] = getNameParts(title);
const splitter = new GraphemeSplitter();
const first = splitter.iterateGraphemes(rawFirst);
const second = rawSecond ? splitter.iterateGraphemes(rawSecond): first;
const third = rawThird ? splitter.iterateGraphemes(rawThird) : iter.newEmpty();
return [
...iter.take(first, 1),
...iter.take(second, 1),
...iter.take(third, 1),
].filter(Boolean).join("");
}
export function Avatar(props: Props) {
const { title, src, width = 32, height = 32, colorHash, ...settings } = props;
const generateAvatarStyle = (): React.CSSProperties => {
return {
backgroundColor: randomColor({ seed: colorHash, luminosity: "dark" }),
width,
height,
textTransform: "uppercase"
};
};
return (
<MaterialAvatar
variant="rounded"
style={generateAvatarStyle()}
{...settings}
>
{getIconString(title)}
</MaterialAvatar>
);
}

View File

@ -112,6 +112,10 @@
}
}
&:focus {
outline: none;
}
&:focus:not(:active) {
transition: box-shadow 350ms;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .5);

View File

@ -28,7 +28,7 @@ import { cssNames } from "../../utils";
interface Props extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
index: number;
innerRef?: React.LegacyRef<HTMLDivElement>;
innerRef?: React.Ref<HTMLDivElement>;
}
export function HotbarCell({ innerRef, children, className, ...rest }: Props) {

View File

@ -20,92 +20,6 @@
*/
.HotbarMenu {
.HotbarIcon {
--size: 37px;
border-radius: 6px;
user-select: none;
cursor: pointer;
transition: none;
text-shadow: 0 0 4px #0000008f;
position: relative;
div.MuiAvatar-colorDefault {
font-weight:500;
text-transform: uppercase;
border-radius: 6px;
}
&.disabled {
opacity: 0.4;
cursor: default;
filter: grayscale(0.7);
&:hover {
&:not(.active) {
box-shadow: none;
}
}
}
&.isDragging {
box-shadow: none;
}
div.MuiAvatar-root {
&.active {
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent);
transition: all 0s 0.8s;
}
&:hover {
&:not(.active) {
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff30;
}
}
}
.led {
position: absolute;
left: 3px;
top: 3px;
background-color: var(--layoutBackground);
border: 1px solid var(--clusterMenuBackground);
border-radius: 50%;
padding: 0px;
width: 8px;
height: 8px;
&.online {
background-color: var(--primary);
box-shadow: 0 0 5px var(--clusterMenuBackground), 0 0 5px var(--primary);
}
}
.badge {
position: absolute;
right: -2px;
bottom: -3px;
margin: -8px;
font-size: var(--font-size-small);
background: var(--clusterMenuBackground);
color: var(--textColorAccent);
padding: 0px;
border-radius: 50%;
border: 2px solid var(--clusterMenuBackground);
width: 15px;
height: 15px;
svg {
width: 13px;
}
}
img {
width: var(--size);
height: var(--size);
}
}
.HotbarIconMenu {
left: 30px;
@ -116,3 +30,90 @@
}
}
}
.HotbarIcon {
border-radius: 6px;
user-select: none;
cursor: pointer;
transition: none;
text-shadow: 0 0 4px #0000008f;
position: relative;
div.MuiAvatar-colorDefault {
font-weight:500;
text-transform: uppercase;
border-radius: 6px;
}
&.disabled {
opacity: 0.4;
cursor: default;
filter: grayscale(0.7);
&:hover {
&:not(.active) {
box-shadow: none;
}
}
}
&.isDragging {
box-shadow: none;
}
div.MuiAvatar-root {
width: var(--size);
height: var(--size);
&.active {
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent);
}
&:hover {
&:not(.active) {
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff30;
}
}
}
.led {
position: absolute;
left: 3px;
top: 3px;
background-color: var(--layoutBackground);
border: 1px solid var(--clusterMenuBackground);
border-radius: 50%;
padding: 0px;
width: 8px;
height: 8px;
&.online {
background-color: var(--primary);
box-shadow: 0 0 5px var(--clusterMenuBackground), 0 0 5px var(--primary);
}
}
.badge {
position: absolute;
right: -2px;
bottom: -3px;
margin: -8px;
font-size: var(--font-size-small);
background: var(--clusterMenuBackground);
color: var(--textColorAccent);
padding: 0px;
border-radius: 50%;
border: 2px solid var(--clusterMenuBackground);
width: 15px;
height: 15px;
svg {
width: 13px;
}
}
img {
width: var(--size);
height: var(--size);
}
}

View File

@ -22,16 +22,14 @@
import "./hotbar-icon.scss";
import React, { DOMAttributes, useState } from "react";
import { Avatar } from "@material-ui/core";
import randomColor from "randomcolor";
import GraphemeSplitter from "grapheme-splitter";
import type { CatalogEntityContextMenu } from "../../../common/catalog";
import { cssNames, IClassName, iter } from "../../utils";
import { cssNames, IClassName } from "../../utils";
import { ConfirmDialog } from "../confirm-dialog";
import { Menu, MenuItem } from "../menu";
import { MaterialTooltip } from "../+catalog/material-tooltip/material-tooltip";
import { observer } from "mobx-react";
import { Avatar } from "../avatar/avatar";
interface Props extends DOMAttributes<HTMLElement> {
uid: string;
@ -44,12 +42,6 @@ interface Props extends DOMAttributes<HTMLElement> {
disabled?: boolean;
}
function generateAvatarStyle(seed: string): React.CSSProperties {
return {
"backgroundColor": randomColor({ seed, luminosity: "dark" })
};
}
function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
if (menuItem.confirm) {
ConfirmDialog.open({
@ -67,22 +59,6 @@ function onMenuItemClick(menuItem: CatalogEntityContextMenu) {
}
}
function getNameParts(name: string): string[] {
const byWhitespace = name.split(/\s+/);
if (byWhitespace.length > 1) {
return byWhitespace;
}
const byDashes = name.split(/[-_]+/);
if (byDashes.length > 1) {
return byDashes;
}
return name.split(/@+/);
}
export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => {
const { uid, title, active, className, source, disabled, onMenuOpen, children, ...rest } = props;
const id = `hotbarIcon-${uid}`;
@ -92,36 +68,18 @@ export const HotbarIcon = observer(({menuItems = [], ...props}: Props) => {
setMenuOpen(!menuOpen);
};
const getIconString = () => {
if (!title) {
return "??";
}
const [rawFirst, rawSecond, rawThird] = getNameParts(title);
const splitter = new GraphemeSplitter();
const first = splitter.iterateGraphemes(rawFirst);
const second = rawSecond ? splitter.iterateGraphemes(rawSecond): first;
const third = rawThird ? splitter.iterateGraphemes(rawThird) : iter.newEmpty();
return [
...iter.take(first, 1),
...iter.take(second, 1),
...iter.take(third, 1),
].filter(Boolean).join("");
};
return (
<div className={cssNames("HotbarIcon flex inline", className, { disabled })}>
<MaterialTooltip title={`${title || "unknown"} (${source || "unknown"})`} placement="right">
<div id={id}>
<Avatar
{...rest}
variant="square"
title={title}
colorHash={`${title}-${source}`}
className={active ? "active" : "default"}
style={generateAvatarStyle(`${title}-${source}`)}
>
{getIconString()}
</Avatar>
width={40}
height={40}
/>
{children}
</div>
</MaterialTooltip>

View File

@ -1,17 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<style type="text/css">
.rect{fill:none!important;}
</style>
<rect class="rect" width="512" height="512"/>
<g>
<path d="M237,496h184.8l-42.6-181.7L237,496z"/>
<path d="M16,392.3V496h194.2l81.2-103.7H16z"/>
<path d="M280.1,130.3L496,235.4v-209L280.1,130.3z"/>
<path d="M443.6,496H496V258.8l-122-59.4L443.6,496z"/>
<path d="M469,16H200.1l-32.4,145L469,16z"/>
<path d="M16,170v201.1h160L16,170z"/>
<path d="M178.5,16H16v120.1l105.9,133.1L178.5,16z"/>
</g>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs><style>.cls-1{fill:none!important;}</style></defs>
<rect class="cls-1" width="512" height="512"/><path d="M193.6,220,308.08,21.76A247.33,247.33,0,0,0,256,16,239.62,239.62,0,0,0,104.32,70l87.84,152.4Z"/><path d="M485,184A240.6,240.6,0,0,0,341,31.84L253.12,184Z"/><path d="M491.2,208H311.44l7,12L432.64,418A237.85,237.85,0,0,0,496,256,243,243,0,0,0,491.2,208Z"/><path d="M173,256,79.36,94C40.24,136.72,16,193.36,16,256a243,243,0,0,0,4.8,48H200.56Z"/><path d="M27,328A240.6,240.6,0,0,0,171,480.16L258.88,328Z"/><path d="M297.52,328l-93.6,162.24A247.33,247.33,0,0,0,256,496a239.62,239.62,0,0,0,151.68-54L319.84,289.6Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 730 B

After

Width:  |  Height:  |  Size: 730 B

View File

@ -21,10 +21,7 @@
.DropFileInput {
&.droppable {
box-shadow: 0 0 0 5px $primary; // fixme: might not work sometimes
> * {
pointer-events: none;
}
box-shadow: inset 0 0 0 6px $primary;
transition: all 0.3s;
}
}

View File

@ -39,15 +39,21 @@ export interface DropFileMeta<T extends HTMLElement = any> {
@observer
export class DropFileInput<T extends HTMLElement = any> extends React.Component<DropFileInputProps> {
@observable dropAreaActive = false;
dragCounter = 0; // Counter preventing firing onDragLeave() too early (https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element)
@autobind()
onDragEnter() {
this.dragCounter++;
this.dropAreaActive = true;
}
@autobind()
onDragLeave() {
this.dropAreaActive = false;
this.dragCounter--;
if (this.dragCounter == 0) {
this.dropAreaActive = false;
}
}
@autobind()

View File

@ -50,6 +50,7 @@
position: relative;
padding: $padding /4 * 3 0;
border-bottom: 1px solid $halfGray;
line-height: 1;
&:after {
content: "";

View File

@ -30,12 +30,13 @@
color: inherit;
background: none;
border: none;
border-radius: $radius;
box-shadow: 0 0 0 1px $halfGray;
border-radius: var(--border-radius);
box-shadow: 0 0 0 1px var(--halfGray);
padding: var(--spacing);
.Icon {
height: $margin * 2;
height: calc(var(--margin) * 2);
width: calc(var(--margin) * 2);
}
}

View File

@ -111,6 +111,10 @@
&.active {
background-color: var(--navSelectedBackground);
}
> .label {
width: 100%;
}
}
}
}
@ -171,10 +175,6 @@
}
}
a {
color: var(--colorInfo);
}
section {
display: flex;
flex-direction: column;
@ -189,16 +189,26 @@
h1, h2 {
color: var(--textColorAccent);
text-transform: uppercase;
}
h1 {
color: var(--textColorAccent);
font-size: 22px;
font-weight: bold;
margin-bottom: 10px;
}
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-24;
color: var(--textColorDimmed);
}
.searchInput>label {
box-shadow: none!important;
}

View File

@ -0,0 +1,65 @@
/**
* 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 "./list.module.css";
import React, { useState } from "react";
import { SearchInput } from "../input";
import type { UseTableOptions } from "react-table";
import { ReactTable } from "../table/react-table";
export type SearchFilter = (item: object) => string | number;
interface Props extends UseTableOptions<any> {
items: object[];
filters: SearchFilter[];
title?: React.ReactNode;
}
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-6">
<div className="mr-6">
{title}
</div>
<div>
<SearchInput
value={search}
theme="round-black"
onChange={setSearch}
className={styles.searchInput}
/>
</div>
</div>
<ReactTable columns={columns} data={filteredData}/>
{filteredData.length == 0 && (
<div className={styles.notFound}>No data found</div>
)}
</>
);
}

View File

@ -35,8 +35,6 @@ html {
position: relative;
min-width: 220px;
* {
color: inherit;
}
@ -50,10 +48,11 @@ html {
&__control {
border: none;
border-radius: $radius;
background: transparent;
background: var(--select-menu-bgc);
min-height: 0;
box-shadow: 0 0 0 1px $halfGray;
cursor: pointer;
line-height: 1;
&--is-focused {
box-shadow: 0 0 0 1px $primary;

View File

@ -0,0 +1,43 @@
.table {
border-spacing: 0;
border-top: thin solid var(--hrColor);
}
.thead {
@apply overflow-hidden font-bold;
border-bottom: thin solid var(--hrColor);
color: var(--textColorAccent);
padding-top: 7px;
height: 33px;
}
.thead .tr {
margin: 0 var(--margin);
}
.tr {
margin: calc(var(--margin) * 1.6) var(--margin);
}
.th {
@apply relative whitespace-nowrap;
padding: 0 var(--padding);
}
.headIcon {
font-weight: normal;
color: var(--textColorAccent);
}
.disabledArrow {
opacity: 0.3;
}
.td {
@apply flex items-center;
padding: 0 var(--padding);
}
.actions {
@apply justify-end;
}

View File

@ -0,0 +1,109 @@
/**
* 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 "./react-table.module.css";
import React from "react";
import { useCallback, useMemo } from "react";
import { useFlexLayout, useSortBy, useTable, UseTableOptions } from "react-table";
import { Icon } from "../icon";
import { cssNames } from "../../utils";
interface Props extends UseTableOptions<any> {
headless?: boolean;
}
export function ReactTable({ columns, data, headless }: Props) {
const defaultColumn = useMemo(
() => ({
minWidth: 20,
width: 100,
}),
[]
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable(
{
columns,
data,
defaultColumn,
},
useFlexLayout,
useSortBy,
);
const RenderRow = useCallback(
({ index, style }) => {
const row = rows[index];
prepareRow(row);
return (
<div
{...row.getRowProps({
style,
})}
className={styles.tr}
>
{row.cells.map((cell, index) => (
<div {...cell.getCellProps()} key={cell.getCellProps().key} className={cssNames(styles.td, columns[index].accessor)}>
{cell.render("Cell")}
</div>
))}
</div>
);
},
[columns, prepareRow, rows]
);
return (
<div {...getTableProps()} className={styles.table}>
{!headless && <div className={styles.thead}>
{headerGroups.map(headerGroup => (
<div {...headerGroup.getHeaderGroupProps()} key={headerGroup.getHeaderGroupProps().key} className={styles.tr}>
{headerGroup.headers.map(column => (
<div {...column.getHeaderProps(column.getSortByToggleProps())} key={column.getHeaderProps().key} className={styles.th}>
{column.render("Header")}
{/* Sort direction indicator */}
<span>
{column.isSorted
? column.isSortedDesc
? <Icon material="arrow_drop_down" small/>
: <Icon material="arrow_drop_up" small/>
: !column.disableSortBy && <Icon material="arrow_drop_down" small className={styles.disabledArrow}/>}
</span>
</div>
))}
</div>
))}
</div>}
<div {...getTableBodyProps()}>
{rows.map((row, index) => RenderRow({index}))}
</div>
</div>
);
}

35
tailwind.config.js Normal file
View File

@ -0,0 +1,35 @@
/**
* 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.
*/
module.exports = {
purge: ["src/**/*.tsx"],
darkMode: "class",
theme: {
fontFamily: {
sans: ["Roboto", "Helvetica", "Arial", "sans-serif"],
},
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};

View File

@ -30,7 +30,8 @@
"node_modules/*",
"types/*"
]
}
},
"plugins": [{ "name": "typescript-plugin-css-modules" }]
},
"ts-node": {
"compilerOptions": {

4
types/mocks.d.ts vendored
View File

@ -36,3 +36,7 @@ declare module "*.ttf" {
const content: string;
export = content;
}
declare module "*.module.css" {
const classes: { [key: string]: string };
export default classes;
}

141
types/react-table.config.d.ts vendored Normal file
View File

@ -0,0 +1,141 @@
/**
* 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 {
UseColumnOrderInstanceProps,
UseColumnOrderState,
UseExpandedHooks,
UseExpandedInstanceProps,
UseExpandedOptions,
UseExpandedRowProps,
UseExpandedState,
UseFiltersColumnOptions,
UseFiltersColumnProps,
UseFiltersInstanceProps,
UseFiltersOptions,
UseFiltersState,
UseGlobalFiltersColumnOptions,
UseGlobalFiltersInstanceProps,
UseGlobalFiltersOptions,
UseGlobalFiltersState,
UseGroupByCellProps,
UseGroupByColumnOptions,
UseGroupByColumnProps,
UseGroupByHooks,
UseGroupByInstanceProps,
UseGroupByOptions,
UseGroupByRowProps,
UseGroupByState,
UsePaginationInstanceProps,
UsePaginationOptions,
UsePaginationState,
UseResizeColumnsColumnOptions,
UseResizeColumnsColumnProps,
UseResizeColumnsOptions,
UseResizeColumnsState,
UseRowSelectHooks,
UseRowSelectInstanceProps,
UseRowSelectOptions,
UseRowSelectRowProps,
UseRowSelectState,
UseRowStateCellProps,
UseRowStateInstanceProps,
UseRowStateOptions,
UseRowStateRowProps,
UseRowStateState,
UseSortByColumnOptions,
UseSortByColumnProps,
UseSortByHooks,
UseSortByInstanceProps,
UseSortByOptions,
UseSortByState
} from "react-table";
declare module "react-table" {
// take this file as-is, or comment out the sections that don't apply to your plugin configuration
export interface TableOptions<D extends Record<string, unknown>>
extends UseExpandedOptions<D>,
UseFiltersOptions<D>,
UseGlobalFiltersOptions<D>,
UseGroupByOptions<D>,
UsePaginationOptions<D>,
UseResizeColumnsOptions<D>,
UseRowSelectOptions<D>,
UseRowStateOptions<D>,
UseSortByOptions<D>,
// note that having Record here allows you to add anything to the options, this matches the spirit of the
// underlying js library, but might be cleaner if it's replaced by a more specific type that matches your
// feature set, this is a safe default.
Record<string, any> {}
export interface Hooks<D extends Record<string, unknown> = Record<string, unknown>>
extends UseExpandedHooks<D>,
UseGroupByHooks<D>,
UseRowSelectHooks<D>,
UseSortByHooks<D> {}
export interface TableInstance<D extends Record<string, unknown> = Record<string, unknown>>
extends UseColumnOrderInstanceProps<D>,
UseExpandedInstanceProps<D>,
UseFiltersInstanceProps<D>,
UseGlobalFiltersInstanceProps<D>,
UseGroupByInstanceProps<D>,
UsePaginationInstanceProps<D>,
UseRowSelectInstanceProps<D>,
UseRowStateInstanceProps<D>,
UseSortByInstanceProps<D> {}
export interface TableState<D extends Record<string, unknown> = Record<string, unknown>>
extends UseColumnOrderState<D>,
UseExpandedState<D>,
UseFiltersState<D>,
UseGlobalFiltersState<D>,
UseGroupByState<D>,
UsePaginationState<D>,
UseResizeColumnsState<D>,
UseRowSelectState<D>,
UseRowStateState<D>,
UseSortByState<D> {}
export interface ColumnInterface<D extends Record<string, unknown> = Record<string, unknown>>
extends UseFiltersColumnOptions<D>,
UseGlobalFiltersColumnOptions<D>,
UseGroupByColumnOptions<D>,
UseResizeColumnsColumnOptions<D>,
UseSortByColumnOptions<D> {}
export interface ColumnInstance<D extends Record<string, unknown> = Record<string, unknown>>
extends UseFiltersColumnProps<D>,
UseGroupByColumnProps<D>,
UseResizeColumnsColumnProps<D>,
UseSortByColumnProps<D> {}
export interface Cell<D extends Record<string, unknown> = Record<string, unknown>>
extends UseGroupByCellProps<D>,
UseRowStateCellProps<D> {}
export interface Row<D extends Record<string, unknown> = Record<string, unknown>>
extends UseExpandedRowProps<D>,
UseGroupByRowProps<D>,
UseRowSelectRowProps<D>,
UseRowStateRowProps<D> {}
}

View File

@ -120,14 +120,20 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
{
test: /\.s?css$/,
use: [
// https://webpack.js.org/plugins/mini-css-extract-plugin/
isDevelopment ? "style-loader" : MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
sourceMap: isDevelopment
modules: {
auto: true,
mode: "local",
localIdentName: "[name]__[local]--[hash:base64:5]",
}
},
},
{
loader: "postcss-loader"
},
{
loader: "sass-loader",
options: {
@ -139,9 +145,9 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
]
},
}
},
}
]
}
},
]
},

832
yarn.lock

File diff suppressed because it is too large Load Diff