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

Merge branch 'master' into extensions-docs

This commit is contained in:
Mario Sarcher 2020-10-29 08:17:43 +01:00
commit f67c8a7fe9
96 changed files with 1133 additions and 626 deletions

View File

@ -149,6 +149,11 @@ jobs:
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
GH_TOKEN: $(GH_TOKEN)
- script: make publish-npm
displayName: Publish npm package
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
env:
NPM_TOKEN: $(NPM_TOKEN)
- bash: |
mkdir -p "$AZURE_CACHE_FOLDER"
tar -czf "$AZURE_CACHE_FOLDER/yarn-cache.tar.gz" "$YARN_CACHE_FOLDER"

View File

@ -1,4 +1,5 @@
module.exports = {
ignorePatterns: ["src/extensions/npm/extensions/api.d.ts"],
overrides: [
{
files: [

11
LICENSE
View File

@ -1,8 +1,13 @@
MIT License
Copyright (c) 2020 Mirantis, Inc.
All rights reserved.
Portions of this software are licensed as follows:
* All content residing under the "docs/" directory of this repository, if that
directory exists, is licensed under "Creative Commons: CC BY-SA 4.0 license".
* All third party components incorporated into the Lens Software are licensed
under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is
available under the "MIT" license as defined below.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -59,10 +59,12 @@ build-extensions:
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), $(MAKE) -C $(dir) build;)
build-npm:
yarn compile:extension-types
yarn npm:fix-package-version
publish-npm: build-npm
cd src/extensions/npm/extensions && npm publish
npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
cd src/extensions/npm/extensions && npm publish --access=public
clean:
ifeq "$(DETECTED_OS)" "Windows"

View File

@ -40,9 +40,10 @@ brew cask install lens
Allows for faster separate re-runs of some of the more involved processes:
1. `yarn dev:main` compiles electron's main process part and start watching files
1. `yarn dev:renderer` compiles electron's renderer part and start watching files
1. `yarn dev-run` runs app in dev-mode and restarts when electron's main process file has changed
1. `yarn dev:main` compiles electron's main process app part
1. `yarn dev:renderer` compiles electron's renderer app part
1. `yarn dev:extension-types` compile declaration types for `@k8slens/extensions`
1. `yarn dev-run` runs app in dev-mode and auto-restart when main process file has changed
## Developer's ~~RTFM~~ recommended list:

51
build/build_tray_icon.ts Normal file
View File

@ -0,0 +1,51 @@
// Generate tray icons from SVG to PNG + different sizes and colors (B&W)
// Command: `yarn build:tray-icons`
import path from "path"
import sharp from "sharp";
import jsdom from "jsdom"
import fs from "fs-extra"
export async function generateTrayIcon(
{
outputFilename = "tray_icon", // e.g. output tray_icon_dark@2x.png
svgIconPath = path.resolve(__dirname, "../src/renderer/components/icon/logo-lens.svg"),
outputFolder = path.resolve(__dirname, "./tray"),
dpiSuffix = "2x",
pixelSize = 32,
shouldUseDarkColors = false, // managed by electron.nativeTheme.shouldUseDarkColors
} = {}) {
outputFilename += shouldUseDarkColors ? "_dark" : ""
dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : ""
const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`)
try {
// Modify .SVG colors
const trayIconColor = shouldUseDarkColors ? "white" : "black";
const svgDom = await jsdom.JSDOM.fromFile(svgIconPath);
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`
const svgIconBuffer = Buffer.from(svgRoot.outerHTML);
// Resize and convert to .PNG
const pngIconBuffer: Buffer = await sharp(svgIconBuffer)
.resize({ width: pixelSize, height: pixelSize })
.png()
.toBuffer();
// Save icon
await fs.writeFile(pngIconDestPath, pngIconBuffer);
console.info(`[DONE]: Tray icon saved at "${pngIconDestPath}"`);
} catch (err) {
console.error(`[ERROR]: ${err}`);
}
}
// Run
const iconSizes: Record<string, number> = {
"1x": 16,
"2x": 32,
"3x": 48,
};
Object.entries(iconSizes).forEach(([dpiSuffix, pixelSize]) => {
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: false });
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: true });
});

BIN
build/tray/tray_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

BIN
build/tray/tray_icon@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

BIN
build/tray/tray_icon@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,26 +1,16 @@
import { LensRendererExtension, Registry } from "@k8slens/extensions";
import { ExamplePage, ExampleIcon } from "./page"
import { LensRendererExtension } from "@k8slens/extensions";
import { ExampleIcon, ExamplePage } from "./page"
import React from "react"
export default class ExampleExtension extends LensRendererExtension {
onActivate() {
console.log('EXAMPLE EXTENSION RENDERER: ACTIVATED', this.getMeta());
}
registerClusterPage(registry: Registry.ClusterPageRegistry) {
this.disposers.push(
registry.add({
path: "/extension-example",
title: "Example Extension",
components: {
Page: () => <ExamplePage extension={this} />,
MenuIcon: ExampleIcon,
}
})
)
}
onDeactivate() {
console.log('EXAMPLE EXTENSION RENDERER: DEACTIVATED', this.getMeta());
}
clusterPages = [
{
path: "/extension-example",
title: "Example Extension",
components: {
Page: () => <ExamplePage extension={this}/>,
MenuIcon: ExampleIcon,
}
}
]
}

View File

@ -1,25 +1,23 @@
import { Registry, LensRendererExtension } from "@k8slens/extensions"
import { LensRendererExtension } from "@k8slens/extensions"
import { MetricsFeature } from "./src/metrics-feature"
import React from "react"
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
registerClusterFeatures(registry: Registry.ClusterFeatureRegistry) {
this.disposers.push(
registry.add({
title: "Metrics Stack",
components: {
Description: () => {
return (
<span>
clusterFeatures = [
{
title: "Metrics Stack",
components: {
Description: () => {
return (
<span>
Enable timeseries data visualization (Prometheus stack) for your cluster.
Install this only if you don't have existing Prometheus stack installed.
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>.
</span>
)
}
},
feature: new MetricsFeature()
})
)
}
)
}
},
feature: new MetricsFeature()
}
]
}

View File

@ -1,21 +1,15 @@
import { Registry, LensRendererExtension } from "@k8slens/extensions";
import { LensRendererExtension } from "@k8slens/extensions";
import React from "react"
import { NodeMenu } from "./src/node-menu"
import { NodeMenu, NodeMenuProps } from "./src/node-menu"
export default class NodeMenuRendererExtension extends LensRendererExtension {
async onActivate() {
console.log("node-menu extension activated")
}
registerKubeObjectMenus(registry: Registry.KubeObjectMenuRegistry) {
this.disposers.push(
registry.add({
kind: "Node",
apiVersions: ["v1"],
components: {
MenuItem: (props) => <NodeMenu {...props} />
}
})
)
}
kubeObjectMenuItems = [
{
kind: "Node",
apiVersions: ["v1"],
components: {
MenuItem: (props: NodeMenuProps) => <NodeMenu {...props} />
}
}
]
}

View File

@ -1,7 +1,10 @@
import React from "react";
import { Component, K8sApi, Navigation} from "@k8slens/extensions"
export function NodeMenu(props: Component.KubeObjectMenuProps<K8sApi.Node>) {
export interface NodeMenuProps extends Component.KubeObjectMenuProps<K8sApi.Node> {
}
export function NodeMenu(props: NodeMenuProps) {
const { object: node, toolbar } = props;
if (!node) return null;
const nodeName = node.getName();

View File

@ -1,31 +1,23 @@
import { Registry, LensRendererExtension } from "@k8slens/extensions";
import { PodShellMenu } from "./src/shell-menu"
import { PodLogsMenu } from "./src/logs-menu"
import { LensRendererExtension } from "@k8slens/extensions";
import { PodShellMenu, PodShellMenuProps } from "./src/shell-menu"
import { PodLogsMenu, PodLogsMenuProps } from "./src/logs-menu"
import React from "react"
export default class PodMenuRendererExtension extends LensRendererExtension {
async onActivate() {
console.log("pod-menu extension activated")
}
registerKubeObjectMenus(registry: Registry.KubeObjectMenuRegistry) {
this.disposers.push(
registry.add({
kind: "Pod",
apiVersions: ["v1"],
components: {
MenuItem: (props) => <PodShellMenu {...props} />
}
})
)
this.disposers.push(
registry.add({
kind: "Pod",
apiVersions: ["v1"],
components: {
MenuItem: (props) => <PodLogsMenu {...props} />
}
})
)
}
kubeObjectMenuItems = [
{
kind: "Pod",
apiVersions: ["v1"],
components: {
MenuItem: (props: PodShellMenuProps) => <PodShellMenu {...props} />
}
},
{
kind: "Pod",
apiVersions: ["v1"],
components: {
MenuItem: (props: PodLogsMenuProps) => <PodLogsMenu {...props} />
}
}
]
}

View File

@ -1,10 +1,10 @@
import React from "react";
import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions";
interface Props extends Component.KubeObjectMenuProps<K8sApi.Pod> {
export interface PodLogsMenuProps extends Component.KubeObjectMenuProps<K8sApi.Pod> {
}
export class PodLogsMenu extends React.Component<Props> {
export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
showLogs(container: K8sApi.IPodContainer) {
Navigation.hideDetails();
const pod = this.props.object;

View File

@ -3,10 +3,10 @@
import React from "react";
import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions";
interface Props extends Component.KubeObjectMenuProps<K8sApi.Pod> {
export interface PodShellMenuProps extends Component.KubeObjectMenuProps<K8sApi.Pod> {
}
export class PodShellMenu extends React.Component<Props> {
export class PodShellMenu extends React.Component<PodShellMenuProps> {
async execShell(container?: string) {
Navigation.hideDetails();
const { object: pod } = this.props

View File

@ -1,23 +1,14 @@
import { LensMainExtension, Registry, windowManager } from "@k8slens/extensions";
import { LensMainExtension, windowManager } from "@k8slens/extensions";
import { supportPageURL } from "./src/support.route";
export default class SupportPageMainExtension extends LensMainExtension {
async onActivate() {
console.log("support page extension activated")
}
async registerAppMenus(registry: Registry.MenuRegistry) {
this.disposers.push(
registry.add({
parentId: "help",
label: "Support",
click() {
windowManager.navigate({
channel: "menu:navigate", // fixme: use windowManager.ensureMainWindow from Tray's PR
url: supportPageURL(),
});
}
})
)
}
appMenus = [
{
parentId: "help",
label: "Support",
click() {
windowManager.navigate(supportPageURL());
}
}
]
}

View File

@ -1,39 +1,31 @@
import React from "react";
import { Component, LensRendererExtension, Navigation, Registry } from "@k8slens/extensions";
import { Component, LensRendererExtension, Navigation } from "@k8slens/extensions";
import { supportPageRoute, supportPageURL } from "./src/support.route";
import { Support } from "./src/support";
export default class SupportPageRendererExtension extends LensRendererExtension {
async onActivate() {
console.log("support page extension activated")
}
globalPages = [
{
...supportPageRoute,
url: supportPageURL(),
hideInMenu: true,
components: {
Page: Support,
}
}
]
registerGlobalPage(registry: Registry.GlobalPageRegistry) {
this.disposers.push(
registry.add({
...supportPageRoute,
url: supportPageURL(),
hideInMenu: true,
components: {
Page: Support,
}
})
)
}
registerStatusBarItem(registry: Registry.StatusBarRegistry) {
this.disposers.push(
registry.add({
item: (
<div
className="flex align-center gaps hover-highlight"
onClick={() => Navigation.navigate(supportPageURL())}
>
<Component.Icon material="help_outline" small />
<span>Support</span>
</div>
)
})
)
}
statusBarItems = [
{
item: (
<div
className="flex align-center gaps hover-highlight"
onClick={() => Navigation.navigate(supportPageURL())}
>
<Component.Icon material="help_outline" small/>
<span>Support</span>
</div>
)
}
]
}

View File

@ -3,13 +3,13 @@
import React from "react"
import { observer } from "mobx-react"
import { CommonVars, Component } from "@k8slens/extensions";
import { App, Component } from "@k8slens/extensions";
@observer
export class Support extends React.Component {
render() {
const { PageLayout } = Component;
const { slackUrl, issuesTrackerUrl } = CommonVars;
const { slackUrl, issuesTrackerUrl } = App;
return (
<PageLayout showOnTop className="Support" header={<h2>Support</h2>}>
<h2>Community Slack Channel</h2>

View File

@ -1,29 +1,23 @@
import { LensRendererExtension, Registry } from "@k8slens/extensions";
import { LensRendererExtension } from "@k8slens/extensions";
import { telemetryPreferencesStore } from "./src/telemetry-preferences-store"
import { TelemetryPreferenceHint, TelemetryPreferenceInput } from "./src/telemetry-preference"
import { tracker } from "./src/tracker"
import React from "react"
export default class TelemetryRendererExtension extends LensRendererExtension {
appPreferences = [
{
title: "Telemetry & Usage Tracking",
components: {
Hint: () => <TelemetryPreferenceHint/>,
Input: () => <TelemetryPreferenceInput telemetry={telemetryPreferencesStore}/>
}
}
];
async onActivate() {
console.log("telemetry extension activated")
tracker.start()
await telemetryPreferencesStore.loadExtension(this)
}
registerAppPreferences(registry: Registry.AppPreferenceRegistry) {
this.disposers.push(
registry.add({
title: "Telemetry & Usage Tracking",
components: {
Hint: () => <TelemetryPreferenceHint />,
Input: () => <TelemetryPreferenceInput telemetry={telemetryPreferencesStore} />
}
})
)
}
onDeactivate() {
console.log("telemetry extension deactivated")
}
}

View File

@ -38,8 +38,9 @@ export class Tracker extends Util.Singleton {
}
this.eventHandlers.push(handler)
EventBus.appEventBus.addListener(handler)
this.reportInterval = setInterval(this.reportData, 60 * 60 * 1000) // report every 1h
this.reportInterval = setInterval(() => {
this.reportData()
}, 60 * 60 * 1000) // report every 1h
}
stop() {

View File

@ -389,13 +389,13 @@ describe("Lens integration tests", () => {
it(`shows ${drawer} drawer`, async () => {
expect(clusterAdded).toBe(true)
await app.client.click(`.sidebar-nav #${drawerId} span.link-text`)
await app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name)
await app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name)
})
}
pages.forEach(({ name, href, expectedSelector, expectedText }) => {
it(`shows ${drawer}->${name} page`, async () => {
expect(clusterAdded).toBe(true)
await app.client.click(`a[href="/${href}"]`)
await app.client.click(`a[href^="/${href}"]`)
await app.client.waitUntilTextExists(expectedSelector, expectedText)
})
})
@ -404,7 +404,7 @@ describe("Lens integration tests", () => {
it(`hides ${drawer} drawer`, async () => {
expect(clusterAdded).toBe(true)
await app.client.click(`.sidebar-nav #${drawerId} span.link-text`)
await expect(app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow()
await expect(app.client.waitUntilTextExists(`a[href^="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow()
})
}
})
@ -440,8 +440,8 @@ describe("Lens integration tests", () => {
it(`creates a pod in ${TEST_NAMESPACE} namespace`, async () => {
expect(clusterAdded).toBe(true)
await app.client.click(".sidebar-nav #workloads span.link-text")
await app.client.waitUntilTextExists('a[href="/pods"]', "Pods")
await app.client.click('a[href="/pods"]')
await app.client.waitUntilTextExists('a[href^="/pods"]', "Pods")
await app.client.click('a[href^="/pods"]')
await app.client.waitUntilTextExists("div.TableCell", "kube-apiserver")
await app.client.click('.Icon.new-dock-tab')
await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource")

View File

@ -13,7 +13,6 @@ export function setup(): Application {
path: AppPaths[process.platform],
startTimeout: 30000,
waitTimeout: 60000,
chromeDriverArgs: ['remote-debugging-port=9222'],
env: {
CICD: "true"
}
@ -27,6 +26,6 @@ export async function tearDown(app: Application) {
try {
process.kill(pid, "SIGKILL");
} catch (e) {
return
console.error(e)
}
}

View File

@ -88,7 +88,7 @@ msgid "Active"
msgstr "Active"
#: src/renderer/components/+add-cluster/add-cluster.tsx:310
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
#: src/renderer/components/cluster-manager/clusters-menu.tsx:127
msgid "Add Cluster"
msgstr "Add Cluster"
@ -219,11 +219,11 @@ msgstr "Allocatable"
msgid "Allow Privilege Escalation"
msgstr "Allow Privilege Escalation"
#: src/renderer/components/+preferences/preferences.tsx:169
#: src/renderer/components/+preferences/preferences.tsx:172
msgid "Allow telemetry & usage tracking"
msgstr "Allow telemetry & usage tracking"
#: src/renderer/components/+preferences/preferences.tsx:161
#: src/renderer/components/+preferences/preferences.tsx:164
msgid "Allow untrusted Certificate Authorities"
msgstr "Allow untrusted Certificate Authorities"
@ -301,6 +301,14 @@ msgstr "Associate clusters and choose the ones you want to access via quick laun
msgid "Auth App Role"
msgstr "Auth App Role"
#: src/renderer/components/+preferences/preferences.tsx:160
msgid "Auto start-up"
msgstr "Auto start-up"
#: src/renderer/components/+preferences/preferences.tsx:161
msgid "Automatically start Lens on login"
msgstr "Automatically start Lens on login"
#: src/renderer/components/error-boundary/error-boundary.tsx:53
#: src/renderer/components/wizard/wizard.tsx:130
msgid "Back"
@ -422,7 +430,7 @@ msgstr "Cancel"
msgid "Capacity"
msgstr "Capacity"
#: src/renderer/components/+preferences/preferences.tsx:160
#: src/renderer/components/+preferences/preferences.tsx:163
msgid "Certificate Trust"
msgstr "Certificate Trust"
@ -817,7 +825,7 @@ msgstr "Desired Healthy"
msgid "Desired number of replicas"
msgstr "Desired number of replicas"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:65
#: src/renderer/components/cluster-manager/clusters-menu.tsx:62
msgid "Disconnect"
msgstr "Disconnect"
@ -831,7 +839,7 @@ msgstr "Disk"
msgid "Disk:"
msgstr "Disk:"
#: src/renderer/components/+preferences/preferences.tsx:165
#: src/renderer/components/+preferences/preferences.tsx:168
msgid "Does not affect cluster communications!"
msgstr "Does not affect cluster communications!"
@ -927,8 +935,8 @@ msgstr "Environment"
msgid "Error stack"
msgstr "Error stack"
#: src/renderer/components/+add-cluster/add-cluster.tsx:89
#: src/renderer/components/+add-cluster/add-cluster.tsx:130
#: src/renderer/components/+add-cluster/add-cluster.tsx:88
#: src/renderer/components/+add-cluster/add-cluster.tsx:129
msgid "Error while adding cluster(s): {0}"
msgstr "Error while adding cluster(s): {0}"
@ -1581,7 +1589,7 @@ msgstr "Namespaces"
msgid "Namespaces: {0}"
msgstr "Namespaces: {0}"
#: src/renderer/components/+preferences/preferences.tsx:164
#: src/renderer/components/+preferences/preferences.tsx:167
msgid "Needed with some corporate proxies that do certificate re-writing."
msgstr "Needed with some corporate proxies that do certificate re-writing."
@ -1798,7 +1806,7 @@ msgstr "Persistent Volume Claims"
msgid "Persistent Volumes"
msgstr "Persistent Volumes"
#: src/renderer/components/+add-cluster/add-cluster.tsx:75
#: src/renderer/components/+add-cluster/add-cluster.tsx:74
msgid "Please select at least one cluster context"
msgstr "Please select at least one cluster context"
@ -2025,8 +2033,8 @@ msgstr "Releases"
#: src/renderer/components/+preferences/preferences.tsx:152
#: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60
#: src/renderer/components/cluster-manager/clusters-menu.tsx:76
#: src/renderer/components/cluster-manager/clusters-menu.tsx:82
#: src/renderer/components/cluster-manager/clusters-menu.tsx:73
#: src/renderer/components/cluster-manager/clusters-menu.tsx:79
#: src/renderer/components/item-object-list/item-list-layout.tsx:179
#: src/renderer/components/menu/menu-actions.tsx:49
#: src/renderer/components/menu/menu-actions.tsx:85
@ -2470,7 +2478,7 @@ msgstr "Set"
msgid "Set quota"
msgstr "Set quota"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:53
#: src/renderer/components/cluster-manager/clusters-menu.tsx:51
msgid "Settings"
msgstr "Settings"
@ -2613,7 +2621,7 @@ msgstr "Submitting.."
msgid "Subsets"
msgstr "Subsets"
#: src/renderer/components/+add-cluster/add-cluster.tsx:122
#: src/renderer/components/+add-cluster/add-cluster.tsx:121
msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr "Successfully imported <0>{0}</0> cluster(s)"
@ -2635,11 +2643,11 @@ msgstr "TLS"
msgid "Taints"
msgstr "Taints"
#: src/renderer/components/+preferences/preferences.tsx:168
#: src/renderer/components/+preferences/preferences.tsx:171
msgid "Telemetry & Usage Tracking"
msgstr "Telemetry & Usage Tracking"
#: src/renderer/components/+preferences/preferences.tsx:171
#: src/renderer/components/+preferences/preferences.tsx:174
msgid "Telemetry & usage data is collected to continuously improve the Lens experience."
msgstr "Telemetry & usage data is collected to continuously improve the Lens experience."
@ -2675,7 +2683,7 @@ msgstr "This field must be a valid path"
msgid "This is the quick launch menu."
msgstr "This is the quick launch menu."
#: src/renderer/components/+preferences/preferences.tsx:163
#: src/renderer/components/+preferences/preferences.tsx:166
msgid "This will make Lens to trust ANY certificate authority without any validations."
msgstr "This will make Lens to trust ANY certificate authority without any validations."
@ -2953,7 +2961,7 @@ msgstr "listKind"
msgid "never"
msgstr "never"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:133
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "new"
msgstr "new"

View File

@ -88,7 +88,7 @@ msgid "Active"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:310
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
#: src/renderer/components/cluster-manager/clusters-menu.tsx:127
msgid "Add Cluster"
msgstr ""
@ -219,11 +219,11 @@ msgstr ""
msgid "Allow Privilege Escalation"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:169
#: src/renderer/components/+preferences/preferences.tsx:172
msgid "Allow telemetry & usage tracking"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:161
#: src/renderer/components/+preferences/preferences.tsx:164
msgid "Allow untrusted Certificate Authorities"
msgstr ""
@ -301,6 +301,14 @@ msgstr ""
msgid "Auth App Role"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:160
msgid "Auto start-up"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:161
msgid "Automatically start Lens on login"
msgstr ""
#: src/renderer/components/error-boundary/error-boundary.tsx:53
#: src/renderer/components/wizard/wizard.tsx:130
msgid "Back"
@ -422,7 +430,7 @@ msgstr ""
msgid "Capacity"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:160
#: src/renderer/components/+preferences/preferences.tsx:163
msgid "Certificate Trust"
msgstr ""
@ -813,7 +821,7 @@ msgstr ""
msgid "Desired number of replicas"
msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:65
#: src/renderer/components/cluster-manager/clusters-menu.tsx:62
msgid "Disconnect"
msgstr ""
@ -827,7 +835,7 @@ msgstr ""
msgid "Disk:"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:165
#: src/renderer/components/+preferences/preferences.tsx:168
msgid "Does not affect cluster communications!"
msgstr ""
@ -923,8 +931,8 @@ msgstr ""
msgid "Error stack"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:89
#: src/renderer/components/+add-cluster/add-cluster.tsx:130
#: src/renderer/components/+add-cluster/add-cluster.tsx:88
#: src/renderer/components/+add-cluster/add-cluster.tsx:129
msgid "Error while adding cluster(s): {0}"
msgstr ""
@ -1572,7 +1580,7 @@ msgstr ""
msgid "Namespaces: {0}"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:164
#: src/renderer/components/+preferences/preferences.tsx:167
msgid "Needed with some corporate proxies that do certificate re-writing."
msgstr ""
@ -1781,7 +1789,7 @@ msgstr ""
msgid "Persistent Volumes"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:75
#: src/renderer/components/+add-cluster/add-cluster.tsx:74
msgid "Please select at least one cluster context"
msgstr ""
@ -2008,8 +2016,8 @@ msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:152
#: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60
#: src/renderer/components/cluster-manager/clusters-menu.tsx:76
#: src/renderer/components/cluster-manager/clusters-menu.tsx:82
#: src/renderer/components/cluster-manager/clusters-menu.tsx:73
#: src/renderer/components/cluster-manager/clusters-menu.tsx:79
#: src/renderer/components/item-object-list/item-list-layout.tsx:179
#: src/renderer/components/menu/menu-actions.tsx:49
#: src/renderer/components/menu/menu-actions.tsx:85
@ -2453,7 +2461,7 @@ msgstr ""
msgid "Set quota"
msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:53
#: src/renderer/components/cluster-manager/clusters-menu.tsx:51
msgid "Settings"
msgstr ""
@ -2596,7 +2604,7 @@ msgstr ""
msgid "Subsets"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:122
#: src/renderer/components/+add-cluster/add-cluster.tsx:121
msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr ""
@ -2618,11 +2626,11 @@ msgstr ""
msgid "Taints"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:168
#: src/renderer/components/+preferences/preferences.tsx:171
msgid "Telemetry & Usage Tracking"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:171
#: src/renderer/components/+preferences/preferences.tsx:174
msgid "Telemetry & usage data is collected to continuously improve the Lens experience."
msgstr ""
@ -2658,7 +2666,7 @@ msgstr ""
msgid "This is the quick launch menu."
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:163
#: src/renderer/components/+preferences/preferences.tsx:166
msgid "This will make Lens to trust ANY certificate authority without any validations."
msgstr ""
@ -2936,7 +2944,7 @@ msgstr ""
msgid "never"
msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:133
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "new"
msgstr ""

View File

@ -89,7 +89,7 @@ msgid "Active"
msgstr "Активный"
#: src/renderer/components/+add-cluster/add-cluster.tsx:310
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
#: src/renderer/components/cluster-manager/clusters-menu.tsx:127
msgid "Add Cluster"
msgstr ""
@ -220,11 +220,11 @@ msgstr ""
msgid "Allow Privilege Escalation"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:169
#: src/renderer/components/+preferences/preferences.tsx:172
msgid "Allow telemetry & usage tracking"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:161
#: src/renderer/components/+preferences/preferences.tsx:164
msgid "Allow untrusted Certificate Authorities"
msgstr ""
@ -302,6 +302,14 @@ msgstr ""
msgid "Auth App Role"
msgstr "Auth App Role"
#: src/renderer/components/+preferences/preferences.tsx:160
msgid "Auto start-up"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:161
msgid "Automatically start Lens on login"
msgstr ""
#: src/renderer/components/error-boundary/error-boundary.tsx:53
#: src/renderer/components/wizard/wizard.tsx:130
msgid "Back"
@ -423,7 +431,7 @@ msgstr "Отмена"
msgid "Capacity"
msgstr "Емкость"
#: src/renderer/components/+preferences/preferences.tsx:160
#: src/renderer/components/+preferences/preferences.tsx:163
msgid "Certificate Trust"
msgstr ""
@ -818,7 +826,7 @@ msgstr ""
msgid "Desired number of replicas"
msgstr "Нужный уровень реплик"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:65
#: src/renderer/components/cluster-manager/clusters-menu.tsx:62
msgid "Disconnect"
msgstr ""
@ -832,7 +840,7 @@ msgstr "Диск"
msgid "Disk:"
msgstr "Диск:"
#: src/renderer/components/+preferences/preferences.tsx:165
#: src/renderer/components/+preferences/preferences.tsx:168
msgid "Does not affect cluster communications!"
msgstr ""
@ -928,8 +936,8 @@ msgstr "Среда"
msgid "Error stack"
msgstr "Стэк ошибки"
#: src/renderer/components/+add-cluster/add-cluster.tsx:89
#: src/renderer/components/+add-cluster/add-cluster.tsx:130
#: src/renderer/components/+add-cluster/add-cluster.tsx:88
#: src/renderer/components/+add-cluster/add-cluster.tsx:129
msgid "Error while adding cluster(s): {0}"
msgstr ""
@ -1582,7 +1590,7 @@ msgstr "Namespaces"
msgid "Namespaces: {0}"
msgstr "Namespaces: {0}"
#: src/renderer/components/+preferences/preferences.tsx:164
#: src/renderer/components/+preferences/preferences.tsx:167
msgid "Needed with some corporate proxies that do certificate re-writing."
msgstr ""
@ -1799,7 +1807,7 @@ msgstr "Persistent Volume Claims"
msgid "Persistent Volumes"
msgstr "Persistent Volumes"
#: src/renderer/components/+add-cluster/add-cluster.tsx:75
#: src/renderer/components/+add-cluster/add-cluster.tsx:74
msgid "Please select at least one cluster context"
msgstr ""
@ -2026,8 +2034,8 @@ msgstr "Релизы"
#: src/renderer/components/+preferences/preferences.tsx:152
#: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60
#: src/renderer/components/cluster-manager/clusters-menu.tsx:76
#: src/renderer/components/cluster-manager/clusters-menu.tsx:82
#: src/renderer/components/cluster-manager/clusters-menu.tsx:73
#: src/renderer/components/cluster-manager/clusters-menu.tsx:79
#: src/renderer/components/item-object-list/item-list-layout.tsx:179
#: src/renderer/components/menu/menu-actions.tsx:49
#: src/renderer/components/menu/menu-actions.tsx:85
@ -2471,7 +2479,7 @@ msgstr "Установлено"
msgid "Set quota"
msgstr "Установить квоту"
#: src/renderer/components/cluster-manager/clusters-menu.tsx:53
#: src/renderer/components/cluster-manager/clusters-menu.tsx:51
msgid "Settings"
msgstr ""
@ -2614,7 +2622,7 @@ msgstr "Применение.."
msgid "Subsets"
msgstr ""
#: src/renderer/components/+add-cluster/add-cluster.tsx:122
#: src/renderer/components/+add-cluster/add-cluster.tsx:121
msgid "Successfully imported <0>{0}</0> cluster(s)"
msgstr ""
@ -2636,11 +2644,11 @@ msgstr "TLS"
msgid "Taints"
msgstr "Метки блокировки"
#: src/renderer/components/+preferences/preferences.tsx:168
#: src/renderer/components/+preferences/preferences.tsx:171
msgid "Telemetry & Usage Tracking"
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:171
#: src/renderer/components/+preferences/preferences.tsx:174
msgid "Telemetry & usage data is collected to continuously improve the Lens experience."
msgstr ""
@ -2676,7 +2684,7 @@ msgstr ""
msgid "This is the quick launch menu."
msgstr ""
#: src/renderer/components/+preferences/preferences.tsx:163
#: src/renderer/components/+preferences/preferences.tsx:166
msgid "This will make Lens to trust ANY certificate authority without any validations."
msgstr ""
@ -2954,7 +2962,7 @@ msgstr ""
msgid "never"
msgstr ""
#: src/renderer/components/cluster-manager/clusters-menu.tsx:133
#: src/renderer/components/cluster-manager/clusters-menu.tsx:130
msgid "new"
msgstr ""

View File

@ -2,7 +2,7 @@
"name": "kontena-lens",
"productName": "Lens",
"description": "Lens - The Kubernetes IDE",
"version": "3.6.6",
"version": "4.0.0-alpha.2",
"main": "static/build/main.js",
"copyright": "© 2020, Mirantis, Inc.",
"license": "MIT",
@ -16,12 +16,12 @@
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"",
"dev:main": "yarn compile:main --watch",
"dev:renderer": "yarn compile:renderer --watch",
"dev:extension-rollup": "yarn compile:extension-rollup --watch",
"dev:extension-types": "yarn compile:extension-types --watch",
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
"compile:main": "webpack --config webpack.main.ts",
"compile:renderer": "webpack --config webpack.renderer.ts",
"compile:i18n": "lingui compile",
"compile:extension-rollup": "rollup --config src/extensions/rollup.config.js",
"compile:extension-types": "rollup --config src/extensions/rollup.config.js",
"npm:fix-package-version": "ts-node build/set_npm_version.ts",
"build:linux": "yarn compile && electron-builder --linux --dir -c.productName=Lens",
"build:mac": "yarn compile && electron-builder --mac --dir -c.productName=Lens",
@ -36,6 +36,7 @@
"download-bins": "concurrently yarn:download:*",
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
"download:helm": "yarn run ts-node build/download_helm.ts",
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
"lint": "eslint $@ --ext js,ts,tsx --max-warnings=0 src/"
},
"config": {
@ -97,6 +98,11 @@
"to": "static/",
"filter": "!**/main.js"
},
{
"from": "build/tray",
"to": "static/icons",
"filter": "*.png"
},
{
"from": "extensions/",
"to": "./extensions/",
@ -186,6 +192,20 @@
"@hapi/call": "^8.0.0",
"@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.12.0",
"@types/crypto-js": "^3.1.47",
"@types/electron-window-state": "^2.0.34",
"@types/fs-extra": "^9.0.1",
"@types/http-proxy": "^1.17.4",
"@types/js-yaml": "^3.12.4",
"@types/jsdom": "^16.2.4",
"@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.155",
"@types/marked": "^0.7.4",
"@types/mock-fs": "^4.10.0",
"@types/node": "^12.12.45",
"@types/proper-lockfile": "^4.1.1",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/tar": "^4.0.3",
"array-move": "^3.0.0",
"chalk": "^4.1.0",
"command-exists": "1.2.9",
@ -198,8 +218,8 @@
"fs-extra": "^9.0.1",
"handlebars": "^4.7.6",
"http-proxy": "^1.18.1",
"immer": "^7.0.5",
"js-yaml": "^3.14.0",
"jsdom": "^16.4.0",
"jsonpath": "^1.0.2",
"lodash": "^4.17.15",
"mac-ca": "^1.0.4",
@ -276,6 +296,7 @@
"@types/request": "^2.48.5",
"@types/request-promise-native": "^1.0.17",
"@types/semver": "^7.2.0",
"@types/sharp": "^0.26.0",
"@types/shelljs": "^0.8.8",
"@types/spdy": "^3.4.4",
"@types/tar": "^4.0.3",
@ -327,7 +348,7 @@
"postinstall-postinstall": "^2.1.0",
"progress-bar-webpack-plugin": "^2.1.0",
"raw-loader": "^4.0.1",
"react": "^16.13.1",
"react": "^16.14.0",
"react-beautiful-dnd": "^13.0.0",
"react-dom": "^16.13.1",
"react-router": "^5.2.0",
@ -339,6 +360,7 @@
"rollup-plugin-ignore-import": "^1.3.2",
"rollup-pluginutils": "^2.8.2",
"sass-loader": "^8.0.2",
"sharp": "^0.26.1",
"spectron": "11.0.0",
"style-loader": "^1.2.1",
"terser-webpack-plugin": "^3.0.3",

View File

@ -1,6 +1,6 @@
import type { WorkspaceId } from "./workspace-store";
import path from "path";
import { app, ipcRenderer, remote, webFrame, webContents } from "electron";
import { app, ipcRenderer, remote, webFrame } from "electron";
import { unlink } from "fs-extra";
import { action, computed, observable, toJS } from "mobx";
import { BaseStore } from "./base-store";
@ -113,7 +113,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
@action
setActive(id: ClusterId) {
this.activeClusterId = id;
this.activeClusterId = this.clusters.has(id) ? id : null;
}
@action
@ -160,7 +160,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
if (cluster) {
this.clusters.delete(clusterId);
if (this.activeClusterId === clusterId) {
this.activeClusterId = null;
this.setActive(null);
}
// remove only custom kubeconfigs (pasted as text)
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {

View File

@ -27,6 +27,7 @@ export interface UserPreferences {
downloadKubectlBinaries?: boolean;
downloadBinariesPath?: string;
kubectlBinariesPath?: string;
openAtLogin?: boolean;
}
export class UserStore extends BaseStore<UserStoreModel> {
@ -38,14 +39,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
migrations: migrations,
});
// track telemetry availability
reaction(() => this.preferences.allowTelemetry, allowed => {
appEventBus.emit({name: "telemetry", action: allowed ? "enabled" : "disabled"})
});
// refresh new contexts
this.whenLoaded.then(this.refreshNewContexts);
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
this.handleOnLoad();
}
@observable lastSeenAppVersion = "0.0.0"
@ -59,8 +53,31 @@ export class UserStore extends BaseStore<UserStoreModel> {
colorTheme: UserStore.defaultTheme,
downloadMirror: "default",
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
openAtLogin: true,
};
protected async handleOnLoad() {
await this.whenLoaded;
// refresh new contexts
this.refreshNewContexts();
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
if (app) {
// track telemetry availability
reaction(() => this.preferences.allowTelemetry, allowed => {
appEventBus.emit({name: "telemetry", action: allowed ? "enabled" : "disabled"})
});
// open at system start-up
reaction(() => this.preferences.openAtLogin, open => {
app.setLoginItemSettings({ openAtLogin: open });
}, {
fireImmediately: true,
});
}
}
get isNewVersion() {
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
}

View File

@ -0,0 +1,14 @@
import { compile } from "path-to-regexp"
export interface IURLParams<P extends object = {}, Q extends object = {}> {
params?: P;
query?: Q;
}
export function buildURL<P extends object = {}, Q extends object = {}>(path: string | any) {
const pathBuilder = compile(String(path));
return function ({ params, query }: IURLParams<P, Q> = {}) {
const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : ""
return pathBuilder(params) + (queryParams ? `?${queryParams}` : "")
}
}

View File

@ -1,8 +1,6 @@
import { action, computed, observable, toJS } from "mobx";
import { BaseStore } from "./base-store";
import { clusterStore } from "./cluster-store"
import { landingURL } from "../renderer/components/+landing-page/landing-page.route";
import { navigate } from "../renderer/navigation";
import { appEventBus } from "./event-bus";
export type WorkspaceId = string;
@ -57,18 +55,13 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
}
@action
setActive(id = WorkspaceStore.defaultId, { redirectToLanding = true, resetActiveCluster = true } = {}) {
setActive(id = WorkspaceStore.defaultId, reset = true) {
if (id === this.currentWorkspaceId) return;
if (!this.getById(id)) {
throw new Error(`workspace ${id} doesn't exist`);
}
this.currentWorkspaceId = id;
if (resetActiveCluster) {
clusterStore.setActive(null)
}
if (redirectToLanding) {
navigate(landingURL())
}
clusterStore.activeClusterId = null; // fixme: handle previously selected cluster from current workspace
}
@action

View File

@ -1,5 +1,4 @@
import { app } from "electron";
import { getAppVersion } from "../../common/utils";
export const version = getAppVersion()
export { isSnap, isWindows, isMac, isLinux, appName } from "../../common/vars"
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars"

View File

@ -10,9 +10,9 @@ import * as EventBus from "./event-bus"
import * as Store from "./stores"
import * as Util from "./utils"
import * as Registry from "../registries"
import * as CommonVars from "../../common/vars";
import * as ClusterFeature from "./cluster-feature"
// TODO: allow to expose windowManager.navigate() as Navigation.navigate() in runtime
export let windowManager: WindowManager;
export {
@ -22,5 +22,4 @@ export {
Store,
Util,
Registry,
CommonVars,
}

View File

@ -36,26 +36,26 @@ export class ExtensionLoader {
loadOnMain() {
logger.info('[EXTENSIONS-LOADER]: load on main')
this.autoloadExtensions((instance: LensMainExtension) => {
instance.registerAppMenus(menuRegistry);
this.autoloadExtensions((extension: LensMainExtension) => {
extension.registerTo(menuRegistry, extension.appMenus)
})
}
loadOnClusterManagerRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
this.autoloadExtensions((instance: LensRendererExtension) => {
instance.registerGlobalPage(globalPageRegistry)
instance.registerAppPreferences(appPreferenceRegistry)
instance.registerClusterFeatures(clusterFeatureRegistry)
instance.registerStatusBarItem(statusBarRegistry)
this.autoloadExtensions((extension: LensRendererExtension) => {
extension.registerTo(globalPageRegistry, extension.globalPages)
extension.registerTo(appPreferenceRegistry, extension.appPreferences)
extension.registerTo(clusterFeatureRegistry, extension.clusterFeatures)
extension.registerTo(statusBarRegistry, extension.statusBarItems)
})
}
loadOnClusterRenderer() {
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
this.autoloadExtensions((instance: LensRendererExtension) => {
instance.registerClusterPage(clusterPageRegistry)
instance.registerKubeObjectMenus(kubeObjectMenuRegistry)
this.autoloadExtensions((extension: LensRendererExtension) => {
extension.registerTo(clusterPageRegistry, extension.clusterPages)
extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems)
})
}

View File

@ -25,10 +25,18 @@ export class ExtensionManager {
return extensionPackagesRoot()
}
get inTreeTargetPath() {
return path.join(this.extensionPackagesRoot, "extensions")
}
get inTreeFolderPath(): string {
return path.resolve(__static, "../extensions");
}
get nodeModulesPath(): string {
return path.join(this.extensionPackagesRoot, "node_modules")
}
get localFolderPath(): string {
return path.join(os.homedir(), ".k8slens", "extensions");
}
@ -39,7 +47,13 @@ export class ExtensionManager {
async load() {
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
await fs.ensureDir(path.join(this.extensionPackagesRoot, "node_modules"))
if (this.inTreeFolderPath !== this.inTreeTargetPath) {
// we need to copy in-tree extensions so that we can symlink them properly on "npm install"
await fs.remove(this.inTreeTargetPath)
await fs.ensureDir(this.inTreeTargetPath)
await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath)
}
await fs.ensureDir(this.nodeModulesPath)
await fs.ensureDir(this.localFolderPath)
return await this.loadExtensions();
}
@ -55,7 +69,7 @@ export class ExtensionManager {
id: manifestJson.name,
version: manifestJson.version,
name: manifestJson.name,
manifestPath: path.join(this.extensionPackagesRoot, "node_modules", manifestJson.name, "package.json"),
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
manifest: manifestJson
}
} catch (err) {
@ -80,14 +94,14 @@ export class ExtensionManager {
async loadExtensions() {
const bundledExtensions = await this.loadBundledExtensions()
const localExtendions = await this.loadFromFolder(this.localFolderPath)
const extensions = bundledExtensions.concat(localExtendions)
const localExtensions = await this.loadFromFolder(this.localFolderPath)
const extensions = bundledExtensions.concat(localExtensions)
return new Map(extensions.map(ext => [ext.id, ext]));
}
async loadBundledExtensions() {
const extensions: InstalledExtension[] = []
const folderPath = this.inTreeFolderPath
const folderPath = this.inTreeTargetPath
const bundledExtensions = getBundledExtensions()
const paths = await fs.readdir(folderPath);
for (const fileName of paths) {

View File

@ -1,6 +1,7 @@
import { readJsonSync } from "fs-extra";
import { action, observable, toJS } from "mobx";
import logger from "../main/logger";
import { BaseRegistry } from "./registries/base-registry";
export type ExtensionId = string | ExtensionPackageJsonPath;
export type ExtensionPackageJsonPath = string;
@ -25,7 +26,7 @@ export interface ExtensionManifest extends ExtensionModel {
export class LensExtension implements ExtensionModel {
public id: ExtensionId;
public updateUrl: string;
protected disposers: Function[] = [];
protected disposers: (() => void)[] = [];
@observable name = "";
@observable description = "";
@ -77,6 +78,14 @@ export class LensExtension implements ExtensionModel {
// mock
}
registerTo<T = any>(registry: BaseRegistry<T>, items: T[] = []) {
const disposers = items.map(item => registry.add(item));
this.disposers.push(...disposers);
return () => {
this.disposers = this.disposers.filter(disposer => !disposers.includes(disposer))
};
}
getMeta() {
return toJS({
id: this.id,

View File

@ -1,12 +1,7 @@
import type { MenuRegistration } from "./registries/menu-registry";
import { observable } from "mobx";
import { LensExtension } from "./lens-extension"
import type { MenuRegistry } from "./registries/menu-registry";
export class LensMainExtension extends LensExtension {
registerAppMenus(registry: MenuRegistry) {
//
}
registerPrometheusProviders(registry: any) {
//
}
@observable.shallow appMenus: MenuRegistration[] = []
}

View File

@ -1,28 +1,12 @@
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectMenuRegistration, PageRegistration, StatusBarRegistration } from "./registries"
import { observable } from "mobx";
import { LensExtension } from "./lens-extension"
import type { GlobalPageRegistry, ClusterPageRegistry, AppPreferenceRegistry, StatusBarRegistry, KubeObjectMenuRegistry, ClusterFeatureRegistry } from "./registries"
export class LensRendererExtension extends LensExtension {
registerGlobalPage(registry: GlobalPageRegistry) {
return
}
registerClusterPage(registry: ClusterPageRegistry) {
return
}
registerAppPreferences(registry: AppPreferenceRegistry) {
return
}
registerClusterFeatures(registry: ClusterFeatureRegistry) {
return
}
registerStatusBarItem(registry: StatusBarRegistry) {
return
}
registerKubeObjectMenus(registry: KubeObjectMenuRegistry) {
return
}
@observable.shallow globalPages: PageRegistration[] = []
@observable.shallow clusterPages: PageRegistration[] = []
@observable.shallow appPreferences: AppPreferenceRegistration[] = []
@observable.shallow clusterFeatures: ClusterFeatureRegistration[] = []
@observable.shallow statusBarItems: StatusBarRegistration[] = []
@observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = []
}

View File

@ -6,6 +6,9 @@
"copyright": "© 2020, Mirantis, Inc.",
"license": "MIT",
"types": "api.d.ts",
"files": [
"api.d.ts"
],
"author": {
"name": "Mirantis, Inc.",
"email": "info@k8slens.dev"

View File

@ -1,11 +1,10 @@
// Extensions API -> Global menu customizations
import type { MenuTopId } from "../../main/menu";
import type { MenuItemConstructorOptions } from "electron";
import { BaseRegistry } from "./base-registry";
export interface MenuRegistration extends MenuItemConstructorOptions {
parentId?: MenuTopId;
parentId: string;
}
export class MenuRegistry extends BaseRegistry<MenuRegistration> {

View File

@ -1,19 +1,19 @@
import { autoUpdater } from "electron-updater"
import logger from "./logger"
export default class AppUpdater {
export class AppUpdater {
static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24 // once a day
protected updateInterval: number = (1000 * 60 * 60 * 24) // once a day
static checkForUpdates() {
return autoUpdater.checkForUpdatesAndNotify()
}
constructor() {
constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) {
autoUpdater.logger = logger
}
public start() {
setInterval(() => {
autoUpdater.checkForUpdatesAndNotify()
}, this.updateInterval)
return autoUpdater.checkForUpdatesAndNotify()
setInterval(AppUpdater.checkForUpdates, this.updateInterval)
return AppUpdater.checkForUpdates();
}
}

View File

@ -10,7 +10,7 @@ import path from "path"
import { LensProxy } from "./lens-proxy"
import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager";
import AppUpdater from "./app-updater"
import { AppUpdater } from "./app-updater"
import { shellSync } from "./shell-sync"
import { getFreePort } from "./port"
import { mangleProxyEnv } from "./proxy-env"
@ -24,45 +24,46 @@ import { extensionLoader } from "../extensions/extension-loader";
import logger from "./logger"
const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number;
let proxyServer: LensProxy;
let clusterManager: ClusterManager;
let windowManager: WindowManager;
app.setName(appName);
if (!process.env.CICD) {
app.setPath("userData", workingDir);
}
let clusterManager: ClusterManager;
let proxyServer: LensProxy;
mangleProxyEnv()
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
}
async function main() {
await shellSync();
app.on("ready", async () => {
logger.info(`🚀 Starting Lens from "${workingDir}"`)
await shellSync();
const updater = new AppUpdater()
updater.start();
registerFileProtocol("static", __static);
// find free port
let proxyPort: number
try {
proxyPort = await getFreePort()
} catch (error) {
logger.error(error)
dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy")
app.quit();
}
// preload configuration from stores
// preload isomorphic stores
await Promise.all([
userStore.load(),
clusterStore.load(),
workspaceStore.load(),
]);
// find free port
try {
proxyPort = await getFreePort()
} catch (error) {
logger.error(error)
dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy")
app.exit();
}
// create cluster manager
clusterManager = new ClusterManager(proxyPort);
@ -72,28 +73,34 @@ async function main() {
} catch (error) {
logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`)
dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`)
app.quit();
app.exit();
}
// create window manager and open app
LensExtensionsApi.windowManager = new WindowManager(proxyPort);
windowManager = new WindowManager(proxyPort);
LensExtensionsApi.windowManager = windowManager; // expose to extensions
extensionLoader.loadOnMain()
extensionLoader.extensions.replace(await extensionManager.load())
extensionLoader.broadcastExtensions()
setTimeout(() => {
appEventBus.emit({name: "app", action: "start"})
appEventBus.emit({ name: "app", action: "start" })
}, 1000)
}
});
app.on("ready", main);
app.on("activate", (event, hasVisibleWindows) => {
logger.info('APP:ACTIVATE', { hasVisibleWindows })
if (!hasVisibleWindows) {
windowManager.initMainWindow();
}
});
app.on("will-quit", async (event) => {
event.preventDefault(); // To allow mixpanel sending to be executed
if (proxyServer) proxyServer.close()
if (clusterManager) clusterManager.stop()
app.exit();
// Quit app on Cmd+Q (MacOS)
app.on("will-quit", (event) => {
logger.info('APP:QUIT');
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
clusterManager?.stop(); // close cluster connections
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
})
// Extensions-api runtime exports

View File

@ -12,11 +12,27 @@ import logger from "./logger";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"
export function initMenu(windowManager: WindowManager) {
autorun(() => buildMenu(windowManager), {
return autorun(() => buildMenu(windowManager), {
delay: 100
});
}
export function showAbout(browserWindow: BrowserWindow) {
const appInfo = [
`${appName}: ${app.getVersion()}`,
`Electron: ${process.versions.electron}`,
`Chrome: ${process.versions.chrome}`,
`Copyright 2020 Mirantis, Inc.`,
]
dialog.showMessageBoxSync(browserWindow, {
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
type: "info",
buttons: ["Close"],
message: `Lens`,
detail: appInfo.join("\r\n")
})
}
export function buildMenu(windowManager: WindowManager) {
function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) {
if (isMac) return [];
@ -32,28 +48,9 @@ export function buildMenu(windowManager: WindowManager) {
return menuItems;
}
function navigate(url: string) {
async function navigate(url: string) {
logger.info(`[MENU]: navigating to ${url}`);
windowManager.navigate({
channel: "menu:navigate",
url: url,
})
}
function showAbout(browserWindow: BrowserWindow) {
const appInfo = [
`${appName}: ${app.getVersion()}`,
`Electron: ${process.versions.electron}`,
`Chrome: ${process.versions.chrome}`,
`Copyright 2020 Mirantis, Inc.`,
]
dialog.showMessageBoxSync(browserWindow, {
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
type: "info",
buttons: ["Close"],
message: `Lens`,
detail: appInfo.join("\r\n")
})
await windowManager.navigate(url);
}
const macAppMenu: MenuItemConstructorOptions = {
@ -80,7 +77,13 @@ export function buildMenu(windowManager: WindowManager) {
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
{
label: 'Quit',
accelerator: 'Cmd+Q',
click() {
app.exit(); // force quit since might be blocked within app.on("will-quit")
}
}
]
};
@ -118,7 +121,9 @@ export function buildMenu(windowManager: WindowManager) {
},
{ type: 'separator' },
{ role: 'quit' }
])
]),
{ type: 'separator' },
{ role: 'close' } // close current window
]
};
@ -158,7 +163,7 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click() {
windowManager.reload({ channel: "menu:reload" });
windowManager.reload();
}
},
{ role: 'toggleDevTools' },
@ -209,7 +214,7 @@ export function buildMenu(windowManager: WindowManager) {
// Modify menu from extensions-api
menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => {
try {
const topMenu = appMenu[parentId].submenu as MenuItemConstructorOptions[];
const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[];
topMenu.push(menuItem);
} catch (err) {
logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem })

126
src/main/tray.ts Normal file
View File

@ -0,0 +1,126 @@
import path from "path"
import packageInfo from "../../package.json"
import { app, dialog, Menu, NativeImage, nativeTheme, Tray } from "electron"
import { autorun } from "mobx";
import { showAbout } from "./menu";
import { AppUpdater } from "./app-updater";
import { WindowManager } from "./window-manager";
import { clusterStore } from "../common/cluster-store";
import { workspaceStore } from "../common/workspace-store";
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route";
import logger from "./logger";
import { isDevelopment } from "../common/vars";
// note: instance of Tray should be saved somewhere, otherwise it disappears
export let tray: Tray;
// refresh icon when MacOS dark/light theme has changed
nativeTheme.on("updated", () => tray?.setImage(getTrayIcon()));
export function getTrayIcon(isDark = nativeTheme.shouldUseDarkColors): string {
return path.resolve(
__static,
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
`tray_icon${isDark ? "_dark" : ""}.png`
)
}
export function initTray(windowManager: WindowManager) {
const dispose = autorun(() => {
try {
const menu = createTrayMenu(windowManager);
buildTray(getTrayIcon(), menu);
} catch (err) {
logger.error(`[TRAY]: building failed: ${err}`);
}
})
return () => {
dispose();
tray?.destroy();
tray = null;
}
}
export function buildTray(icon: string | NativeImage, menu: Menu) {
if (!tray) {
tray = new Tray(icon)
tray.setToolTip(packageInfo.description)
tray.setIgnoreDoubleClickEvents(true);
}
tray.setImage(icon);
tray.setContextMenu(menu);
return tray;
}
export function createTrayMenu(windowManager: WindowManager): Menu {
return Menu.buildFromTemplate([
{
label: "About Lens",
async click() {
// note: argument[1] (browserWindow) not available when app is not focused / hidden
const browserWindow = await windowManager.ensureMainWindow();
showAbout(browserWindow);
},
},
{ type: 'separator' },
{
label: "Open Lens",
async click() {
await windowManager.ensureMainWindow()
},
},
{
label: "Preferences",
click() {
windowManager.navigate(preferencesURL());
},
},
{
label: "Clusters",
submenu: workspaceStore.workspacesList
.filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces
.map(workspace => {
const clusters = clusterStore.getByWorkspaceId(workspace.id);
return {
label: workspace.name,
toolTip: workspace.description,
submenu: clusters.map(cluster => {
const { id: clusterId, preferences: { clusterName: label }, online, workspace } = cluster;
return {
label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`,
toolTip: clusterId,
async click() {
workspaceStore.setActive(workspace);
clusterStore.setActive(clusterId);
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
}
}
})
}
}),
},
{
label: "Check for updates",
async click() {
const result = await AppUpdater.checkForUpdates();
if (!result) {
const browserWindow = await windowManager.ensureMainWindow();
dialog.showMessageBoxSync(browserWindow, {
message: "No updates available",
type: "info",
})
}
},
},
{ type: 'separator' },
{
label: 'Quit App',
click() {
app.exit();
}
}
]);
}

View File

@ -1,95 +1,140 @@
import type { ClusterId } from "../common/cluster-store";
import { clusterStore } from "../common/cluster-store";
import { BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"
import windowStateKeeper from "electron-window-state"
import { observable } from "mobx";
import { initMenu } from "./menu";
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"
import windowStateKeeper from "electron-window-state"
import { extensionLoader } from "../extensions/extension-loader";
import { appEventBus } from "../common/event-bus"
import { initMenu } from "./menu";
import { initTray } from "./tray";
export class WindowManager {
protected mainView: BrowserWindow;
protected mainWindow: BrowserWindow;
protected splashWindow: BrowserWindow;
protected windowState: windowStateKeeper.State;
protected disposers: Record<string, Function> = {};
@observable activeClusterId: ClusterId;
constructor(protected proxyPort: number) {
this.bindEvents();
this.initMenu();
this.initTray();
this.initMainWindow();
}
get mainUrl() {
return `http://localhost:${this.proxyPort}`
}
async initMainWindow(showSplash = true) {
// Manage main window size and position with state persistence
this.windowState = windowStateKeeper({
defaultHeight: 900,
defaultWidth: 1440,
});
if (!this.windowState) {
this.windowState = windowStateKeeper({
defaultHeight: 900,
defaultWidth: 1440,
});
}
if (!this.mainWindow) {
// show icon in dock (mac-os only)
app.dock?.show();
const { width, height, x, y } = this.windowState;
this.mainView = new BrowserWindow({
x, y, width, height,
show: false,
minWidth: 700, // accommodate 800 x 600 display minimum
minHeight: 500, // accommodate 800 x 600 display minimum
titleBarStyle: "hidden",
backgroundColor: "#1e2124",
webPreferences: {
nodeIntegration: true,
nodeIntegrationInSubFrames: true,
enableRemoteModule: true,
},
});
this.windowState.manage(this.mainView);
const { width, height, x, y } = this.windowState;
this.mainWindow = new BrowserWindow({
x, y, width, height,
show: false,
minWidth: 700, // accommodate 800 x 600 display minimum
minHeight: 500, // accommodate 800 x 600 display minimum
titleBarStyle: "hidden",
backgroundColor: "#1e2124",
webPreferences: {
nodeIntegration: true,
nodeIntegrationInSubFrames: true,
enableRemoteModule: true,
},
});
this.windowState.manage(this.mainWindow);
// open external links in default browser (target=_blank, window.open)
this.mainView.webContents.on("new-window", (event, url) => {
event.preventDefault();
shell.openExternal(url);
});
this.mainView.webContents.on("dom-ready", () => {
extensionLoader.broadcastExtensions()
})
this.mainView.on("focus", () => {
appEventBus.emit({name: "app", action: "focus"})
})
this.mainView.on("blur", () => {
appEventBus.emit({name: "app", action: "blur"})
})
// open external links in default browser (target=_blank, window.open)
this.mainWindow.webContents.on("new-window", (event, url) => {
event.preventDefault();
shell.openExternal(url);
});
this.mainWindow.webContents.on("dom-ready", () => {
extensionLoader.broadcastExtensions()
})
this.mainWindow.on("focus", () => {
appEventBus.emit({name: "app", action: "focus"})
})
this.mainWindow.on("blur", () => {
appEventBus.emit({name: "app", action: "blur"})
})
// clean up
this.mainWindow.on("closed", () => {
this.windowState.unmanage();
this.mainWindow = null;
this.splashWindow = null;
app.dock?.hide(); // hide icon in dock (mac-os)
})
}
try {
if (showSplash) await this.showSplash();
await this.mainWindow.loadURL(this.mainUrl);
this.mainWindow.show();
this.splashWindow?.close();
} catch (err) {
dialog.showErrorBox("ERROR!", err.toString())
}
}
protected async initMenu() {
this.disposers.menuAutoUpdater = initMenu(this);
}
protected initTray() {
this.disposers.trayAutoUpdater = initTray(this);
}
protected bindEvents() {
// track visible cluster from ui
ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => {
this.activeClusterId = clusterId;
});
// load & show app
this.showMain();
initMenu(this);
}
navigate({ url, channel, frameId }: { url: string, channel: string, frameId?: number }) {
async ensureMainWindow(): Promise<BrowserWindow> {
if (!this.mainWindow) await this.initMainWindow();
this.mainWindow.show();
return this.mainWindow;
}
sendToView({ channel, frameId, data = [] }: { channel: string, frameId?: number, data?: any[] }) {
if (frameId) {
this.mainView.webContents.sendToFrame(frameId, channel, url);
this.mainWindow.webContents.sendToFrame(frameId, channel, ...data);
} else {
this.mainView.webContents.send(channel, url);
this.mainWindow.webContents.send(channel, ...data);
}
}
reload({ channel }: { channel: string }) {
async navigate(url: string, frameId?: number) {
await this.ensureMainWindow();
this.sendToView({
channel: "menu:navigate",
frameId: frameId,
data: [url],
})
}
reload() {
const frameId = clusterStore.getById(this.activeClusterId)?.frameId;
if (frameId) {
this.mainView.webContents.sendToFrame(frameId, channel);
this.sendToView({ channel: "menu:reload", frameId });
} else {
webContents.getFocusedWebContents()?.reload();
}
}
async showMain() {
try {
await this.showSplash();
await this.mainView.loadURL(`http://localhost:${this.proxyPort}`)
this.mainView.show()
this.splashWindow.close()
} catch (err) {
dialog.showErrorBox("ERROR!", err.toString())
}
}
async showSplash() {
if (!this.splashWindow) {
this.splashWindow = new BrowserWindow({
@ -110,8 +155,13 @@ export class WindowManager {
}
destroy() {
this.windowState.unmanage();
this.mainWindow.destroy();
this.splashWindow.destroy();
this.mainView.destroy();
this.mainWindow = null;
this.splashWindow = null;
Object.entries(this.disposers).forEach(([name, dispose]) => {
dispose();
delete this.disposers[name]
});
}
}

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const addClusterRoute: RouteProps = {
path: "/add-cluster"

View File

@ -46,6 +46,7 @@ export class AddCluster extends React.Component {
@observable dropAreaActive = false;
componentDidMount() {
clusterStore.setActive(null);
this.setKubeConfig(userStore.kubeConfigPath);
}
@ -118,17 +119,16 @@ export class AddCluster extends React.Component {
}
}
@action
addClusters = () => {
const configValidationErrors:string[] = [];
let newClusters: ClusterModel[] = [];
try {
if (!this.selectedContexts.length) {
this.error = <Trans>Please select at least one cluster context</Trans>
return;
}
this.error = ""
this.isWaiting = true
this.isWaiting = true
newClusters = this.selectedContexts.filter(context => {
try {
@ -138,8 +138,8 @@ export class AddCluster extends React.Component {
} catch (err) {
this.error = String(err.message)
if (err instanceof ExecValidationNotFoundError ) {
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
return false;
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
return false;
} else {
throw new Error(err);
}
@ -169,7 +169,7 @@ export class AddCluster extends React.Component {
clusterStore.setActive(clusterId);
navigate(clusterViewURL({ params: { clusterId } }));
} else {
if (newClusters.length > 1) {
if (newClusters.length > 1) {
Notifications.ok(
<Trans>Successfully imported <b>{newClusters.length}</b> cluster(s)</Trans>
);

View File

@ -1,6 +1,6 @@
import { RouteProps } from "react-router"
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
import { appsRoute } from "../+apps/apps.route";
import { buildURL } from "../../navigation";
export const helmChartsRoute: RouteProps = {
path: appsRoute.path + "/charts/:repo?/:chartName?"

View File

@ -1,6 +1,6 @@
import { RouteProps } from "react-router"
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
import { appsRoute } from "../+apps/apps.route";
import { buildURL } from "../../navigation";
export const releaseRoute: RouteProps = {
path: appsRoute.path + "/releases/:namespace?/:name?"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const appsRoute: RouteProps = {
path: "/apps",

View File

@ -1,6 +1,6 @@
import type { IClusterViewRouteParams } from "../cluster-manager/cluster-view.route";
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export interface IClusterSettingsRouteParams extends IClusterViewRouteParams {
}

View File

@ -1,8 +1,9 @@
import "./cluster-settings.scss";
import React from "react";
import { autorun } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { reaction } from "mobx";
import { RouteComponentProps } from "react-router";
import { observer, disposeOnUnmount } from "mobx-react";
import { Features } from "./features";
import { Removal } from "./removal";
import { Status } from "./status";
@ -11,7 +12,6 @@ import { Cluster } from "../../../main/cluster";
import { ClusterIcon } from "../cluster-icon";
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
import { clusterStore } from "../../../common/cluster-store";
import { RouteComponentProps } from "react-router";
import { clusterIpc } from "../../../common/cluster-ipc";
import { PageLayout } from "../layout/page-layout";
@ -20,16 +20,23 @@ interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
@observer
export class ClusterSettings extends React.Component<Props> {
get cluster(): Cluster {
return clusterStore.getById(this.props.match.params.clusterId);
get clusterId() {
return this.props.match.params.clusterId
}
async componentDidMount() {
disposeOnUnmount(this,
autorun(() => {
this.refreshCluster();
get cluster(): Cluster {
return clusterStore.getById(this.clusterId);
}
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => this.cluster, this.refreshCluster, {
fireImmediately: true,
}),
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
fireImmediately: true,
})
)
])
}
refreshCluster = async () => {

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const clusterRoute: RouteProps = {
path: "/cluster"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const hpaRoute: RouteProps = {
path: "/hpa"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const configMapsRoute: RouteProps = {
path: "/configmaps"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const pdbRoute: RouteProps = {
path: "/poddisruptionbudgets"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const resourceQuotaRoute: RouteProps = {
path: "/resourcequotas"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const secretsRoute: RouteProps = {
path: "/secrets"

View File

@ -1,7 +1,7 @@
import { RouteProps } from "react-router";
import { configMapsURL } from "../+config-maps";
import { Config } from "./config";
import { IURLParams } from "../../navigation";
import { IURLParams } from "../../../common/utils/buildUrl";
import { configMapsURL } from "../+config-maps/config-maps.route";
export const configRoute: RouteProps = {
get path() {

View File

@ -10,8 +10,8 @@ import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config
import { PodDisruptionBudgets, pdbRoute, pdbURL } from "../+config-pod-disruption-budgets";
import { configURL } from "./config.route";
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
import { buildURL } from "../../navigation";
import { isAllowedResource } from "../../../common/rbac"
import { buildURL } from "../../../common/utils/buildUrl";
export const certificatesURL = buildURL("/certificates");
export const issuersURL = buildURL("/issuers");

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const crdRoute: RouteProps = {
path: "/crd"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const eventRoute: RouteProps = {
path: "/events"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const landingRoute: RouteProps = {
path: "/landing"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const namespacesRoute: RouteProps = {
path: "/namespaces"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const endpointRoute: RouteProps = {
path: "/endpoints"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const ingressRoute: RouteProps = {
path: "/ingresses"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const networkPoliciesRoute: RouteProps = {
path: "/network-policies"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const servicesRoute: RouteProps = {
path: "/services"

View File

@ -1,7 +1,7 @@
import { RouteProps } from "react-router"
import { Network } from "./network";
import { servicesURL } from "../+network-services";
import { IURLParams } from "../../navigation";
import { IURLParams } from "../../../common/utils/buildUrl";
export const networkRoute: RouteProps = {
get path() {

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const nodesRoute: RouteProps = {
path: "/nodes"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const podSecurityPoliciesRoute: RouteProps = {
path: "/pod-security-policies"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const preferencesRoute: RouteProps = {
path: "/preferences"

View File

@ -21,4 +21,8 @@
display: none;
}
}
.Checkbox {
align-self: start; // limit clickable area to checkbox + text
}
}

View File

@ -156,6 +156,13 @@ export class Preferences extends React.Component {
})}
</div>
<h2><Trans>Auto start-up</Trans></h2>
<Checkbox
label={<Trans>Automatically start Lens on login</Trans>}
value={preferences.openAtLogin}
onChange={v => preferences.openAtLogin = v}
/>
<h2><Trans>Certificate Trust</Trans></h2>
<Checkbox
label={<Trans>Allow untrusted Certificate Authorities</Trans>}

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const storageClassesRoute: RouteProps = {
path: "/storage-classes"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const volumeClaimsRoute: RouteProps = {
path: "/persistent-volume-claims"

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router"
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const volumesRoute: RouteProps = {
path: "/persistent-volumes"

View File

@ -1,7 +1,7 @@
import { RouteProps } from "react-router"
import { volumeClaimsURL } from "../+storage-volume-claims";
import { Storage } from "./storage";
import { IURLParams } from "../../navigation";
import { IURLParams } from "../../../common/utils/buildUrl";
export const storageRoute: RouteProps = {
get path() {

View File

@ -1,6 +1,6 @@
import { RouteProps } from "react-router";
import type { RouteProps } from "react-router";
import { buildURL, IURLParams } from "../../../common/utils/buildUrl";
import { UserManagement } from "./user-management"
import { buildURL, IURLParams } from "../../navigation";
export const usersManagementRoute: RouteProps = {
get path() {
@ -30,9 +30,7 @@ export interface IRolesRouteParams {
}
// URL-builders
export const usersManagementURL = (params?: IURLParams) => serviceAccountsURL(params);
export const serviceAccountsURL = buildURL<IServiceAccountsRouteParams>(serviceAccountsRoute.path)
export const roleBindingsURL = buildURL<IRoleBindingsRouteParams>(roleBindingsRoute.path)
export const rolesURL = buildURL<IRoleBindingsRouteParams>(rolesRoute.path)
export const usersManagementURL = (params?: IURLParams) => {
return serviceAccountsURL(params);
};

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const whatsNewRoute: RouteProps = {
path: "/what-s-new"

View File

@ -1,7 +1,7 @@
import { RouteProps } from "react-router"
import { Workloads } from "./workloads";
import { buildURL, IURLParams } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL, IURLParams } from "../../../common/utils/buildUrl";
import { KubeResource } from "../../../common/rbac";
import { Workloads } from "./workloads";
export const workloadsRoute: RouteProps = {
get path() {

View File

@ -1,5 +1,5 @@
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export const workspacesRoute: RouteProps = {
path: "/workspaces"

View File

@ -1,4 +1,5 @@
import "./cluster-manager.scss"
import React from "react";
import { Redirect, Route, Switch } from "react-router";
import { comparer, reaction } from "mobx";
@ -11,14 +12,17 @@ import { Workspaces, workspacesRoute } from "../+workspaces";
import { AddCluster, addClusterRoute } from "../+add-cluster";
import { ClusterView } from "./cluster-view";
import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
import { clusterViewRoute, clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route";
import { clusterViewRoute, clusterViewURL } from "./cluster-view.route";
import { clusterStore } from "../../../common/cluster-store";
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
import { getMatchedClusterId } from "../../navigation";
@observer
export class ClusterManager extends React.Component {
componentDidMount() {
const getMatchedCluster = () => clusterStore.getById(getMatchedClusterId());
disposeOnUnmount(this, [
reaction(getMatchedClusterId, initView, {
fireImmediately: true
@ -55,7 +59,7 @@ export class ClusterManager extends React.Component {
return (
<div className="ClusterManager">
<main>
<div id="lens-views" />
<div id="lens-views"/>
<Switch>
<Route component={LandingPage} {...landingRoute} />
<Route component={Preferences} {...preferencesRoute} />
@ -66,11 +70,11 @@ export class ClusterManager extends React.Component {
{globalPageRegistry.getItems().map(({ path, url = String(path), components: { Page } }) => {
return <Route key={url} path={path} component={Page}/>
})}
<Redirect exact to={this.startUrl} />
<Redirect exact to={this.startUrl}/>
</Switch>
</main>
<ClustersMenu />
<BottomBar />
<ClustersMenu/>
<BottomBar/>
</div>
)
}

View File

@ -1,8 +1,5 @@
import { reaction } from "mobx";
import { ipcRenderer } from "electron";
import { matchPath, RouteProps } from "react-router";
import { buildURL, navigation } from "../../navigation";
import { clusterStore } from "../../../common/cluster-store";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export interface IClusterViewRouteParams {
clusterId: string;
@ -14,33 +11,3 @@ export const clusterViewRoute: RouteProps = {
}
export const clusterViewURL = buildURL<IClusterViewRouteParams>(clusterViewRoute.path)
export function getMatchedClusterId(): string {
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
exact: true,
path: clusterViewRoute.path
})
if (matched) {
return matched.params.clusterId;
}
}
export function getMatchedCluster() {
return clusterStore.getById(getMatchedClusterId())
}
if (ipcRenderer) {
if (process.isMainFrame) {
// Keep track of active cluster-id for handling IPC/menus/etc.
reaction(() => getMatchedClusterId(), clusterId => {
ipcRenderer.send("cluster-view:current-id", clusterId);
}, {
fireImmediately: true
})
}
// Reload dashboard
ipcRenderer.on("menu:reload", () => {
location.reload();
});
}

View File

@ -1,14 +1,37 @@
import "./cluster-view.scss"
import React from "react";
import { observer } from "mobx-react";
import { getMatchedCluster } from "./cluster-view.route";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { RouteComponentProps } from "react-router";
import { IClusterViewRouteParams } from "./cluster-view.route";
import { ClusterStatus } from "./cluster-status";
import { hasLoadedView } from "./lens-views";
import { Cluster } from "../../../main/cluster";
import { clusterStore } from "../../../common/cluster-store";
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
}
@observer
export class ClusterView extends React.Component {
export class ClusterView extends React.Component<Props> {
get clusterId() {
return this.props.match.params.clusterId;
}
get cluster(): Cluster {
return clusterStore.getById(this.clusterId);
}
async componentDidMount() {
disposeOnUnmount(this, [
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
fireImmediately: true,
})
])
}
render() {
const cluster = getMatchedCluster();
const { cluster } = this;
const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id) || !cluster.ready)
return (
<div className="ClusterView">

View File

@ -1,8 +1,9 @@
import type { Cluster } from "../../../main/cluster";
import "./clusters-menu.scss"
import { remote } from "electron"
import React from "react";
import { remote } from "electron"
import type { Cluster } from "../../../main/cluster";
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
import { observer } from "mobx-react";
import { _i18n } from "../../i18n";
import { t, Trans } from "@lingui/macro";
@ -21,7 +22,6 @@ import { Tooltip } from "../tooltip";
import { ConfirmDialog } from "../confirm-dialog";
import { clusterIpc } from "../../../common/cluster-ipc";
import { clusterViewURL } from "./cluster-view.route";
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
interface Props {
@ -31,13 +31,11 @@ interface Props {
@observer
export class ClustersMenu extends React.Component<Props> {
showCluster = (clusterId: ClusterId) => {
clusterStore.setActive(clusterId);
navigate(clusterViewURL({ params: { clusterId } }));
}
addCluster = () => {
navigate(addClusterURL());
clusterStore.setActive(null);
}
showContextMenu = (cluster: Cluster) => {
@ -47,7 +45,6 @@ export class ClustersMenu extends React.Component<Props> {
menu.append(new MenuItem({
label: _i18n._(t`Settings`),
click: () => {
clusterStore.setActive(cluster.id);
navigate(clusterSettingsURL({
params: {
clusterId: cluster.id
@ -112,21 +109,14 @@ export class ClustersMenu extends React.Component<Props> {
<div className="clusters flex column gaps">
<DragDropContext onDragEnd={this.swapClusterIconOrder}>
<Droppable droppableId="cluster-menu" type="CLUSTER">
{(provided: DroppableProvided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
>
{({ innerRef, droppableProps, placeholder }: DroppableProvided) => (
<div ref={innerRef} {...droppableProps}>
{clusters.map((cluster, index) => {
const isActive = cluster.id === clusterStore.activeClusterId;
return (
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
{(provided: DraggableProvided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => (
<div ref={innerRef} {...draggableProps} {...dragHandleProps}>
<ClusterIcon
key={cluster.id}
showErrors={true}
@ -138,9 +128,9 @@ export class ClustersMenu extends React.Component<Props> {
</div>
)}
</Draggable>
)}
)}
{provided.placeholder}
)
})}
{placeholder}
</div>
)}
</Droppable>
@ -150,16 +140,14 @@ export class ClustersMenu extends React.Component<Props> {
<Tooltip targetId="add-cluster-icon">
<Trans>Add Cluster</Trans>
</Tooltip>
<Icon big material="add" id="add-cluster-icon" />
<Icon big material="add" id="add-cluster-icon"/>
{newContexts.size > 0 && (
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>} />
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/>
)}
</div>
<div className="extensions">
{globalPageRegistry.getItems().map(({ path, url = String(path), hideInMenu, components: { MenuIcon } }) => {
if (!MenuIcon || hideInMenu) {
return;
}
if (!MenuIcon || hideInMenu) return;
return <MenuIcon key={url} onClick={() => navigate(url)}/>
})}
</div>

View File

@ -1,6 +1,6 @@
import { observable, when } from "mobx";
import { ClusterId, clusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
import { getMatchedCluster } from "./cluster-view.route"
import { getMatchedClusterId } from "../../navigation";
import logger from "../../../main/logger";
export interface LensView {
@ -51,7 +51,7 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame
}
export function refreshViews() {
const cluster = getMatchedCluster();
const cluster = clusterStore.getById(getMatchedClusterId());
lensViews.forEach(({ clusterId, view, isLoaded }) => {
const isCurrent = clusterId === cluster?.id;
const isReady = cluster?.available && cluster?.ready;

View File

Before

Width:  |  Height:  |  Size: 730 B

After

Width:  |  Height:  |  Size: 730 B

View File

@ -1,7 +0,0 @@
<svg id="Layer_1" viewBox="0 0 194 219" xmlns="http://www.w3.org/2000/svg">
<g fill="#fff">
<polygon points="98 12.4 71 28.3 44 44.1 17 60 17 91.8 17 123.6 17 155.4 44 171.3 44 139.5 44 107.7 44 75.9 71 60 71 91.8 71 91.8 71 123.6 98 107.7 98 107.7 125 91.8 125 60 152 44.1 125 28.3 98 12.4"/>
<polygon points="152 44.1 152 75.9 152 107.7 125 123.6 152 139.5 152 171.3 179 155.4 179 123.6 179 91.8 179 60 152 44.1"/>
<polygon points="98 139.5 71 155.4 71 155.4 71 187.2 98 203.1 125 187.2 125 155.4 98 139.5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 538 B

View File

@ -80,7 +80,7 @@ export class Sidebar extends React.Component<Props> {
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>
<div className="header flex align-center">
<NavLink exact to="/" className="box grow">
<Icon svg="logo-full" className="logo-icon"/>
<Icon svg="logo-lens" className="logo-icon"/>
<div className="logo-text">Lens</div>
</NavLink>
<Icon

View File

@ -1,22 +1,16 @@
// Navigation helpers
import { ipcRenderer } from "electron";
import { compile } from "path-to-regexp"
import { createBrowserHistory, createMemoryHistory, LocationDescriptor } from "history";
import { matchPath } from "react-router";
import { reaction } from "mobx";
import { createObservableHistory } from "mobx-observable-history";
import { createBrowserHistory, createMemoryHistory, LocationDescriptor } from "history";
import logger from "../main/logger";
import { clusterViewRoute, IClusterViewRouteParams } from "./components/cluster-manager/cluster-view.route";
export const history = typeof window !== "undefined" ? createBrowserHistory() : createMemoryHistory();
export const navigation = createObservableHistory(history);
// handle navigation from other process (e.g. system menus in main, common->cluster view interactions)
if (ipcRenderer) {
ipcRenderer.on("menu:navigate", (event, location: LocationDescriptor) => {
logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event);
navigate(location);
})
}
export function navigate(location: LocationDescriptor) {
const currentLocation = navigation.getPath();
navigation.push(location);
@ -25,20 +19,6 @@ export function navigate(location: LocationDescriptor) {
}
}
export interface IURLParams<P = {}, Q = {}> {
params?: P;
query?: IQueryParams & Q;
}
// todo: extract building urls to commons (also used in menu.ts)
// fixme: missing types validation for params & query
export function buildURL<P extends object, Q = object>(path: string | string[]) {
const pathBuilder = compile(path.toString());
return function ({ params, query }: IURLParams<P, Q> = {}) {
return pathBuilder(params) + (query ? getQueryString(query, false) : "")
}
}
// common params for all pages
export interface IQueryParams {
namespaces?: string[]; // selected context namespaces
@ -100,3 +80,33 @@ export function setSearch(text: string) {
export function getSearch() {
return navigation.searchParams.get("search") || "";
}
export function getMatchedClusterId(): string {
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
exact: true,
path: clusterViewRoute.path
});
return matched?.params.clusterId;
}
//-- EVENTS
if (process.isMainFrame) {
// Keep track of active cluster-id for handling IPC/menus/etc.
reaction(() => getMatchedClusterId(), clusterId => {
ipcRenderer.send("cluster-view:current-id", clusterId);
}, {
fireImmediately: true
})
}
// Handle navigation via IPC (e.g. from top menu)
ipcRenderer.on("menu:navigate", (event, location: LocationDescriptor) => {
logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event);
navigate(location);
});
// Reload dashboard window
ipcRenderer.on("menu:reload", () => {
location.reload();
});

View File

@ -2,7 +2,17 @@
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
## 3.6.6 (current version)
## 4.0.0-alpha.2 (current version)
- Extension API
- Improved pod logs
- Tray icon
- Add last-status information for container
- Add LoadBalancer information to Ingress view
- Move tracker to an extension
- Add support page (as an extension)
## 3.6.6
- Fix labels' word boundary to cover only drawer badges
- Fix cluster dashboard opening not to start authentication proxy twice
- Fix: Refresh cluster connection status also when connection is disconnected

241
yarn.lock
View File

@ -1941,6 +1941,15 @@
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.4.tgz#7d3b534ec35a0585128e2d332db1403ebe057e25"
integrity sha512-fYMgzN+9e28R81weVN49inn/u798ruU91En1ZnGvSZzCRc5jXx9B2EDhlRaWmcO1RIxFHL8AajRXzxDuJu93+A==
"@types/jsdom@^16.2.4":
version "16.2.4"
resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.4.tgz#527ca99943e00561ca4056b1904fd5f4facebc3b"
integrity sha512-RssgLa5ptjVKRkHho/Ex0+DJWkVsYuV8oh2PSG3gKxFp8n/VNyB7kOrZGQkk2zgPlcBkIKOItUc/T5BXit9uhg==
dependencies:
"@types/node" "*"
"@types/parse5" "*"
"@types/tough-cookie" "*"
"@types/json-schema@^7.0.3":
version "7.0.5"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
@ -2059,6 +2068,11 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/parse5@*":
version "5.0.3"
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
"@types/podium@*":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/podium/-/podium-1.0.0.tgz#bfaa2151be2b1d6109cc69f7faa9dac2cba3bb20"
@ -2199,6 +2213,13 @@
dependencies:
"@types/node" "*"
"@types/sharp@^0.26.0":
version "0.26.0"
resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.26.0.tgz#2fa8419dbdaca8dd38f73888b27b207f188a8669"
integrity sha512-oJrR8eiwpL7qykn2IeFRduXM4za7z+7yOUEbKVtuDQ/F6htDLHYO6IbzhaJQHV5n6O3adIh4tJvtgPyLyyydqg==
dependencies:
"@types/node" "*"
"@types/shelljs@^0.8.8":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.8.tgz#e439c69929b88a2c8123c1a55e09eb708315addf"
@ -3247,6 +3268,15 @@ bl@^1.0.0:
readable-stream "^2.3.5"
safe-buffer "^5.1.1"
bl@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489"
integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==
dependencies:
buffer "^5.5.0"
inherits "^2.0.4"
readable-stream "^3.4.0"
block-stream@*:
version "0.0.9"
resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
@ -4647,6 +4677,20 @@ decompress-response@^3.3.0:
dependencies:
mimic-response "^1.0.0"
decompress-response@^4.2.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986"
integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==
dependencies:
mimic-response "^2.0.0"
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
dependencies:
mimic-response "^3.1.0"
deep-extend@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
@ -4741,6 +4785,11 @@ detect-indent@~5.0.0:
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50=
detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
detect-newline@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
@ -5124,7 +5173,7 @@ encoding@^0.1.11:
dependencies:
iconv-lite "^0.6.2"
end-of-stream@^1.0.0, end-of-stream@^1.1.0:
end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
@ -5485,6 +5534,11 @@ expand-brackets@^2.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
expand-template@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
expand-tilde@^2.0.0, expand-tilde@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
@ -6131,6 +6185,11 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
github-from-package@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
@ -6739,11 +6798,6 @@ ignore@^5.1.4:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
immer@^7.0.5:
version "7.0.5"
resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.5.tgz#8af347db5b60b40af8ae7baf1784ea4d35b5208e"
integrity sha512-TtRAKZyuqld2eYjvWgXISLJ0ZlOl1OOTzRmrmiY8SlB0dnAhZ1OiykIDL5KDFNaPHDXiLfGQFNJGtet8z8AEmg==
import-fresh@^3.0.0, import-fresh@^3.1.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66"
@ -7867,6 +7921,38 @@ jsdom@^16.2.2:
ws "^7.2.3"
xml-name-validator "^3.0.0"
jsdom@^16.4.0:
version "16.4.0"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.4.0.tgz#36005bde2d136f73eee1a830c6d45e55408edddb"
integrity sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==
dependencies:
abab "^2.0.3"
acorn "^7.1.1"
acorn-globals "^6.0.0"
cssom "^0.4.4"
cssstyle "^2.2.0"
data-urls "^2.0.0"
decimal.js "^10.2.0"
domexception "^2.0.1"
escodegen "^1.14.1"
html-encoding-sniffer "^2.0.1"
is-potential-custom-element-name "^1.0.0"
nwsapi "^2.2.0"
parse5 "5.1.1"
request "^2.88.2"
request-promise-native "^1.0.8"
saxes "^5.0.0"
symbol-tree "^3.2.4"
tough-cookie "^3.0.1"
w3c-hr-time "^1.0.2"
w3c-xmlserializer "^2.0.0"
webidl-conversions "^6.1.0"
whatwg-encoding "^1.0.5"
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
ws "^7.2.3"
xml-name-validator "^3.0.0"
jsesc@^2.5.1:
version "2.5.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
@ -8810,6 +8896,16 @@ mimic-response@^1.0.0, mimic-response@^1.0.1:
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
mimic-response@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
mini-create-react-context@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz#df60501c83151db69e28eac0ef08b4002efab040"
@ -8845,7 +8941,7 @@ minimatch@^3.0.4, minimatch@~3.0.2:
dependencies:
brace-expansion "^1.1.7"
minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
@ -8930,6 +9026,11 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
mkdirp-classic@^0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
@ -9037,6 +9138,11 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"
napi-build-utils@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -9060,6 +9166,18 @@ no-case@^3.0.3:
lower-case "^2.0.1"
tslib "^1.10.0"
node-abi@^2.7.0:
version "2.19.1"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.1.tgz#6aa32561d0a5e2fdb6810d8c25641b657a8cea85"
integrity sha512-HbtmIuByq44yhAzK7b9j/FelKlHYISKQn0mtvcBrU5QBkhoCMp5bu8Hv5AI34DcKfOAcJBcOEMwLlwO62FFu9A==
dependencies:
semver "^5.4.1"
node-addon-api@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.2.tgz#04bc7b83fd845ba785bb6eae25bc857e1ef75681"
integrity sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg==
node-fetch-npm@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz#6507d0e17a9ec0be3bec516958a497cec54bf5a4"
@ -9237,6 +9355,11 @@ nodemon@^2.0.4:
undefsafe "^2.0.2"
update-notifier "^4.0.0"
noop-logger@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"
integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=
"nopt@2 || 3":
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
@ -9559,7 +9682,7 @@ npm@^6.14.8:
worker-farm "^1.7.0"
write-file-atomic "^2.4.3"
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.1.2, npmlog@~4.1.2:
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.1, npmlog@^4.1.2, npmlog@~4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@ -10371,6 +10494,27 @@ postinstall-postinstall@^2.1.0:
resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3"
integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ==
prebuild-install@^5.3.5:
version "5.3.5"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.5.tgz#e7e71e425298785ea9d22d4f958dbaccf8bb0e1b"
integrity sha512-YmMO7dph9CYKi5IR/BzjOJlRzpxGGVo1EsLSUZ0mt/Mq0HWZIHOKHHcHdT69yG54C9m6i45GpItwRHpk0Py7Uw==
dependencies:
detect-libc "^1.0.3"
expand-template "^2.0.3"
github-from-package "0.0.0"
minimist "^1.2.3"
mkdirp "^0.5.1"
napi-build-utils "^1.0.1"
node-abi "^2.7.0"
noop-logger "^0.1.1"
npmlog "^4.0.1"
pump "^3.0.0"
rc "^1.2.7"
simple-get "^3.0.3"
tar-fs "^2.0.0"
tunnel-agent "^0.6.0"
which-pm-runs "^1.0.0"
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@ -10680,7 +10824,7 @@ raw-loader@^4.0.1:
loader-utils "^2.0.0"
schema-utils "^2.6.5"
rc@^1.0.1, rc@^1.1.6, rc@^1.2.8:
rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
@ -10802,7 +10946,16 @@ react-zlib-js@^1.0.4:
resolved "https://registry.yarnpkg.com/react-zlib-js/-/react-zlib-js-1.0.4.tgz#dd2b9fbf56d5ab224fa7a99affbbedeba9aa3dc7"
integrity sha512-ynXD9DFxpE7vtGoa3ZwBtPmZrkZYw2plzHGbanUjBOSN4RtuXdektSfABykHtTiWEHMh7WdYj45LHtp228ZF1A==
react@^16.13.1, react@^16.8.0:
react@^16.14.0:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
react@^16.8.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
@ -10944,7 +11097,7 @@ read@1, read@~1.0.1, read@~1.0.7:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0:
readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
@ -11649,6 +11802,21 @@ shallow-clone@^3.0.0:
dependencies:
kind-of "^6.0.2"
sharp@^0.26.1:
version "0.26.1"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.26.1.tgz#084e3447ba17f1baf3e3f2e08305ed7aec236ce9"
integrity sha512-9MhwS4ys8pnwBH7MtnBdLzUv+cb24QC4xbzzQL6A+1MQ4Se2V6oPHEX8TIGIZUPRKi6S1kJPVNzt/Xqqp6/H3Q==
dependencies:
color "^3.1.2"
detect-libc "^1.0.3"
node-addon-api "^3.0.2"
npmlog "^4.1.2"
prebuild-install "^5.3.5"
semver "^7.3.2"
simple-get "^4.0.0"
tar-fs "^2.1.0"
tunnel-agent "^0.6.0"
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@ -11701,6 +11869,29 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-get@^3.0.3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3"
integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==
dependencies:
decompress-response "^4.2.0"
once "^1.3.1"
simple-concat "^1.0.0"
simple-get@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.0.tgz#73fa628278d21de83dadd5512d2cc1f4872bd675"
integrity sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ==
dependencies:
decompress-response "^6.0.0"
once "^1.3.1"
simple-concat "^1.0.0"
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
@ -12354,6 +12545,16 @@ tapable@^1.0.0, tapable@^1.1.3:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
tar-fs@^2.0.0, tar-fs@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5"
integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"
pump "^3.0.0"
tar-stream "^2.0.0"
tar-stream@^1.5.0:
version "1.6.2"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
@ -12367,6 +12568,17 @@ tar-stream@^1.5.0:
to-buffer "^1.1.1"
xtend "^4.0.0"
tar-stream@^2.0.0:
version "2.1.4"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa"
integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw==
dependencies:
bl "^4.0.3"
end-of-stream "^1.4.1"
fs-constants "^1.0.0"
inherits "^2.0.3"
readable-stream "^3.1.1"
tar@^2.0.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40"
@ -13272,7 +13484,7 @@ webidl-conversions@^5.0.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==
webidl-conversions@^6.0.0:
webidl-conversions@^6.0.0, webidl-conversions@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
@ -13367,6 +13579,11 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which-pm-runs@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
which@1, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"