1
0
mirror of https://github.com/lensapp/lens.git synced 2024-09-20 05:47:24 +03:00

Installing extensions UI improvements (#1522)

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-11-25 16:42:19 +02:00 committed by GitHub
parent 73724b5a54
commit 7243dfdce4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 186 additions and 217 deletions

View File

@ -225,7 +225,6 @@
"electron-devtools-installer": "^3.1.1",
"electron-updater": "^4.3.1",
"electron-window-state": "^5.0.3",
"file-type": "^14.7.1",
"filenamify": "^4.1.0",
"fs-extra": "^9.0.1",
"handlebars": "^4.7.6",

View File

@ -3,6 +3,7 @@ import request from "request";
export interface DownloadFileOptions {
url: string;
gzip?: boolean;
timeout?: number;
}
export interface DownloadFileTicket {
@ -11,9 +12,9 @@ export interface DownloadFileTicket {
cancel(): void;
}
export function downloadFile({ url, gzip = true }: DownloadFileOptions): DownloadFileTicket {
export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions): DownloadFileTicket {
const fileChunks: Buffer[] = [];
const req = request(url, { gzip });
const req = request(url, { gzip, timeout });
const promise: Promise<Buffer> = new Promise((resolve, reject) => {
req.on("data", (chunk: Buffer) => {
fileChunks.push(chunk);

View File

@ -2,8 +2,7 @@ import React from "react";
import { observable, autorun } from "mobx";
import { observer, disposeOnUnmount } from "mobx-react";
import { Cluster } from "../../../../main/cluster";
import { Input } from "../../input";
import { isUrl } from "../../input/input_validators";
import { Input, InputValidators } from "../../input";
import { SubTitle } from "../../layout/sub-title";
interface Props {
@ -41,7 +40,7 @@ export class ClusterProxySetting extends React.Component<Props> {
onChange={this.onChange}
onBlur={this.save}
placeholder="http://<address>:<port>"
validators={isUrl}
validators={this.proxy ? InputValidators.isUrl : undefined}
/>
</>
);

View File

@ -1,61 +1,36 @@
.Extensions {
.PageLayout.Extensions {
$spacing: $padding * 2;
--width: 100%;
--max-width: auto;
--width: 50%;
.extensions-list {
.extension {
--flex-gap: $padding / 3;
padding: $padding $spacing;
background: $colorVague;
border-radius: $radius;
h2 {
margin-bottom: $padding;
}
&:not(:first-of-type) {
margin-top: $spacing;
}
.no-extensions {
--flex-gap: #{$padding};
padding: $padding;
code {
font-size: $font-size-small;
}
}
.extensions-info {
.install-extension {
margin: $spacing * 2 0;
}
.installed-extensions {
--flex-gap: #{$spacing};
> .flex.gaps {
--flex-gap: #{$padding};
}
}
.extensions-path {
word-break: break-all;
&:hover code {
color: $textColorSecondary;
cursor: pointer;
}
}
.Clipboard {
display: inline;
vertical-align: baseline;
font-size: $font-size-small;
&:hover {
color: $textColorSecondary;
.extension {
padding: $padding $spacing;
background: $layoutBackground;
border-radius: $radius;
}
}
.SearchInput {
--spacing: #{$padding};
width: 100%;
max-width: none;
}
.WizardLayout {
padding: 0;
.info-col {
flex: 0.6;
align-self: flex-start;
}
--spacing: 10px;
}
}

View File

@ -9,8 +9,7 @@ import { observer } from "mobx-react";
import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { Button } from "../button";
import { WizardLayout } from "../layout/wizard-layout";
import { DropFileInput, Input, InputValidators, SearchInput } from "../input";
import { DropFileInput, Input, InputValidator, InputValidators, SearchInput } from "../input";
import { Icon } from "../icon";
import { SubTitle } from "../layout/sub-title";
import { PageLayout } from "../layout/page-layout";
@ -21,6 +20,8 @@ import { LensExtensionManifest, sanitizeExtensionName } from "../../../extension
import { Notifications } from "../notifications";
import { downloadFile, extractTar, listTarEntries, readFileFromTar } from "../../../common/utils";
import { docsUrl } from "../../../common/vars";
import { prevDefault } from "../../utils";
import { TooltipPosition } from "../tooltip";
interface InstallRequest {
fileName: string;
@ -40,8 +41,16 @@ interface InstallRequestValidated extends InstallRequestPreloaded {
@observer
export class Extensions extends React.Component {
private supportedFormats = [".tar", ".tgz"];
private installPathValidator: InputValidator = {
message: <Trans>Invalid URL or absolute path</Trans>,
validate(value: string) {
return InputValidators.isUrl.validate(value) || InputValidators.isPath.validate(value);
}
};
@observable search = "";
@observable downloadUrl = "";
@observable installPath = "";
@computed get extensions() {
const searchText = this.search.toLowerCase();
@ -87,25 +96,25 @@ export class Extensions extends React.Component {
}
};
addExtensions = () => {
const { downloadUrl } = this;
if (downloadUrl && InputValidators.isUrl.validate(downloadUrl)) {
this.installFromUrl(downloadUrl);
} else {
this.installFromSelectFileDialog();
}
};
installFromUrl = async (url: string) => {
installFromUrlOrPath = async () => {
const { installPath } = this;
if (!installPath) return;
const fileName = path.basename(installPath);
try {
const { promise: filePromise } = downloadFile({ url });
this.requestInstall([{
fileName: path.basename(url),
data: await filePromise,
}]);
// install via url
// fixme: improve error messages for non-tar-file URLs
if (InputValidators.isUrl.validate(installPath)) {
const { promise: filePromise } = downloadFile({ url: installPath, timeout: 60000 /*1m*/ });
const data = await filePromise;
this.requestInstall({ fileName, data });
}
// otherwise installing from system path
else if (InputValidators.isPath.validate(installPath)) {
this.requestInstall({ fileName, filePath: installPath });
}
} catch (err) {
Notifications.error(
<p>Installation via URL has failed: <b>{String(err)}</b></p>
<p>Installation has failed: <b>{String(err)}</b></p>
);
}
};
@ -198,7 +207,8 @@ export class Extensions extends React.Component {
return validatedRequests;
}
async requestInstall(requests: InstallRequest[]) {
async requestInstall(init: InstallRequest | InstallRequest[]) {
const requests = Array.isArray(init) ? init : [init];
const preloadedRequests = await this.preloadExtensions(requests);
const validatedRequests = await this.createTempFilesAndValidate(preloadedRequests);
@ -265,49 +275,16 @@ export class Extensions extends React.Component {
}
}
renderInfo() {
return (
<div className="extensions-info flex column gaps">
<h2>Lens Extensions</h2>
<div>
The features that Lens includes out-of-the-box are just the start.
Lens extensions let you add new features to your installation to support your workflow.
Rich extensibility model lets extension authors plug directly into the Lens UI and contribute functionality through the same APIs used by Lens itself.
Check out documentation to <a href={`${docsUrl}/latest/extensions/usage/`} target="_blank">learn more</a>.
</div>
<div className="install-extension flex column gaps">
<SubTitle title="Install extension:"/>
<Input
showErrorsAsTooltip={true}
className="box grow"
theme="round-black"
iconLeft="link"
placeholder={`URL to an extension package (${this.supportedFormats.join(", ")})`}
validators={InputValidators.isUrl}
value={this.downloadUrl}
onChange={v => this.downloadUrl = v}
onSubmit={this.addExtensions}
/>
<Button
primary
label="Install"
onClick={this.addExtensions}
/>
<p className="hint">
<Trans><b>Pro-Tip</b>: you can drag & drop extension's tarball here to request installation</Trans>
</p>
</div>
</div>
);
}
renderExtensions() {
const { extensions, extensionsPath, search } = this;
if (!extensions.length) {
return (
<div className="flex align-center box grow justify-center gaps">
{search && <Trans>No search results found</Trans>}
{!search && <p><Trans>There are no extensions in</Trans> <code>{extensionsPath}</code></p>}
<div className="no-extensions flex box gaps justify-center">
<Icon material="info"/>
<div>
{search && <p>No search results found</p>}
{!search && <p>There are no extensions in <code>{extensionsPath}</code></p>}
</div>
</div>
);
}
@ -316,11 +293,11 @@ export class Extensions extends React.Component {
const { name, description } = manifest;
return (
<div key={extId} className="extension flex gaps align-center">
<div className="box grow flex column gaps">
<div className="package">
<div className="box grow">
<div className="name">
Name: <code className="name">{name}</code>
</div>
<div>
<div className="description">
Description: <span className="text-secondary">{description}</span>
</div>
</div>
@ -336,21 +313,64 @@ export class Extensions extends React.Component {
}
render() {
const topHeader = <h2>Manage Lens Extensions</h2>;
const { installPath } = this;
return (
<PageLayout showOnTop className="Extensions" header={<h2>Extensions</h2>}>
<DropFileInput onDropFiles={this.installOnDrop}>
<WizardLayout infoPanel={this.renderInfo()}>
<DropFileInput onDropFiles={this.installOnDrop}>
<PageLayout showOnTop className="Extensions flex column gaps" header={topHeader} contentGaps={false}>
<h2>Lens Extensions</h2>
<div>
The features that Lens includes out-of-the-box are just the start.
Lens extensions let you add new features to your installation to support your workflow.
Rich extensibility model lets extension authors plug directly into the Lens UI and contribute functionality through the same APIs used by Lens itself.
Check out documentation to <a href={`${docsUrl}/latest/extensions/usage/`} target="_blank">learn more</a>.
</div>
<div className="install-extension flex column gaps">
<SubTitle title={<Trans>Install Extension:</Trans>}/>
<div className="extension-input flex box gaps align-center">
<Input
className="box grow"
theme="round-black"
placeholder={`Path or URL to an extension package (${this.supportedFormats.join(", ")})`}
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
validators={installPath ? this.installPathValidator : undefined}
value={installPath}
onChange={v => this.installPath = v}
onSubmit={this.installFromUrlOrPath}
iconLeft="link"
iconRight={
<Icon
interactive
material="folder"
onMouseDown={prevDefault(this.installFromSelectFileDialog)}
tooltip={<Trans>Browse</Trans>}
/>
}
/>
</div>
<Button
primary
label="Install"
disabled={!this.installPathValidator.validate(installPath)}
onClick={this.installFromUrlOrPath}
/>
<small className="hint">
<Trans><b>Pro-Tip</b>: you can drag & drop extension's tarball-file to install</Trans>
</small>
</div>
<h2>Installed Extensions</h2>
<div className="installed-extensions flex column gaps">
<SearchInput
placeholder={_i18n._(t`Search installed extensions`)}
placeholder="Search extensions by name or description"
value={this.search}
onChange={(value) => this.search = value}
/>
<div className="extensions-list">
{this.renderExtensions()}
</div>
</WizardLayout>
</DropFileInput>
</PageLayout>
{this.renderExtensions()}
</div>
</PageLayout>
</DropFileInput>
);
}
}

View File

@ -1,8 +1,7 @@
import React, { useState } from 'react';
import { Trans } from '@lingui/macro';
import { isPath } from '../input/input_validators';
import { Checkbox } from '../checkbox';
import { Input } from '../input';
import { Input, InputValidators } from '../input';
import { SubTitle } from '../layout/sub-title';
import { UserPreferences, userStore } from '../../../common/user-store';
import { observer } from 'mobx-react';
@ -12,6 +11,7 @@ import { SelectOption, Select } from '../select';
export const KubectlBinaries = observer(({ preferences }: { preferences: UserPreferences }) => {
const [downloadPath, setDownloadPath] = useState(preferences.downloadBinariesPath || "");
const [binariesPath, setBinariesPath] = useState(preferences.kubectlBinariesPath || "");
const pathValidator = downloadPath ? InputValidators.isPath : undefined;
const downloadMirrorOptions: SelectOption<string>[] = [
{ value: "default", label: "Default (Google)" },
@ -47,7 +47,7 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
theme="round-black"
value={downloadPath}
placeholder={userStore.getDefaultKubectlPath()}
validators={isPath}
validators={pathValidator}
onChange={setDownloadPath}
onBlur={save}
disabled={!preferences.downloadKubectlBinaries}
@ -60,7 +60,7 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
theme="round-black"
placeholder={bundledKubectlPath()}
value={binariesPath}
validators={isPath}
validators={pathValidator}
onChange={setBinariesPath}
onBlur={save}
disabled={preferences.downloadKubectlBinaries}

View File

@ -60,7 +60,7 @@ export const PodLogSearch = observer((props: PodLogSearchProps) => {
<SearchInput
value={searchQuery}
onChange={setSearch}
closeIcon={false}
showClearIcon={false}
contentRight={totalFinds > 0 && findCounts}
onClear={onClear}
onKeyDown={onKeyDown}

View File

@ -61,7 +61,7 @@ export class DropFileInput<T extends HTMLElement = any> extends React.Component<
const isValidContentElem = React.isValidElement(contentElem);
if (isValidContentElem) {
const contentElemProps: React.HTMLProps<HTMLElement> = {
className: cssNames("DropFileInput", className, {
className: cssNames("DropFileInput", contentElem.props.className, className, {
droppable: this.dropAreaActive,
}),
onDragEnter,

View File

@ -89,8 +89,10 @@
&.theme {
&.round-black {
&.invalid label {
border-color: $colorSoftError !important;
&.invalid.dirty {
label {
border-color: $colorSoftError;
}
}
label {

View File

@ -3,13 +3,13 @@ import "./input.scss";
import React, { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react";
import { autobind, cssNames, debouncePromise, getRandId } from "../../utils";
import { Icon } from "../icon";
import { Tooltip, TooltipProps } from "../tooltip";
import * as Validators from "./input_validators";
import { InputValidator } from "./input_validators";
import isString from "lodash/isString";
import isFunction from "lodash/isFunction";
import isBoolean from "lodash/isBoolean";
import uniqueId from "lodash/uniqueId";
import { Tooltip } from "../tooltip";
const { conditionalValidators, ...InputValidators } = Validators;
export { InputValidators, InputValidator };
@ -26,7 +26,7 @@ export type InputProps<T = string> = Omit<InputElementProps, "onChange" | "onSub
maxRows?: number; // when multiLine={true} define max rows size
dirty?: boolean; // show validation errors even if the field wasn't touched yet
showValidationLine?: boolean; // show animated validation line for async validators
showErrorsAsTooltip?: boolean; // show validation errors as a tooltip :hover (instead of block below)
showErrorsAsTooltip?: boolean | Omit<TooltipProps, "targetId">; // show validation errors as a tooltip :hover (instead of block below)
iconLeft?: string | React.ReactNode; // material-icon name in case of string-type
iconRight?: string | React.ReactNode;
contentRight?: string | React.ReactNode; // Any component of string goes after iconRight
@ -63,6 +63,10 @@ export class Input extends React.Component<InputProps, State> {
errors: [],
};
isValid() {
return this.state.valid;
}
setValue(value: string) {
if (value !== this.getValue()) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(this.input.constructor.prototype, "value").set;
@ -268,7 +272,8 @@ export class Input extends React.Component<InputProps, State> {
render() {
const {
multiLine, showValidationLine, validators, theme, maxRows, children, showErrorsAsTooltip,
maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight,
maxLength, rows, disabled, autoSelectOnFocus, iconLeft, iconRight, contentRight, id,
dirty: _dirty, // excluded from passing to input-element
...inputProps
} = this.props;
const { focused, dirty, valid, validating, errors } = this.state;
@ -294,29 +299,35 @@ export class Input extends React.Component<InputProps, State> {
ref: this.bindRef,
spellCheck: "false",
});
const tooltipId = showErrorsAsTooltip ? getRandId({ prefix: "input_tooltip_id" }) : undefined;
const showErrors = errors.length > 0 && !valid && dirty;
const errorsInfo = (
<div className="errors box grow">
{errors.map((error, i) => <p key={i}>{error}</p>)}
</div>
);
const componentId = id || showErrorsAsTooltip ? getRandId({ prefix: "input_tooltip_id" }) : undefined;
let tooltipError: React.ReactNode;
if (showErrorsAsTooltip && showErrors) {
const tooltipProps = typeof showErrorsAsTooltip === "object" ? showErrorsAsTooltip : {};
tooltipProps.className = cssNames("InputTooltipError", tooltipProps.className);
tooltipError = (
<Tooltip targetId={componentId} {...tooltipProps}>
<div className="flex gaps align-center">
<Icon material="error_outline"/>
{errorsInfo}
</div>
</Tooltip>
);
}
return (
<div id={tooltipId} className={className}>
<div id={componentId} className={className}>
{tooltipError}
<label className="input-area flex gaps align-center" id="">
{isString(iconLeft) ? <Icon material={iconLeft}/> : iconLeft}
{multiLine ? <textarea {...inputProps as any} /> : <input {...inputProps as any} />}
{isString(iconRight) ? <Icon material={iconRight}/> : iconRight}
{contentRight}
</label>
{showErrorsAsTooltip && showErrors && (
<Tooltip targetId={tooltipId} className="InputTooltipError">
<div className="flex gaps align-center">
<Icon material="error_outline"/>
{errorsInfo}
</div>
</Tooltip>
)}
<div className="input-info flex gaps">
{!showErrorsAsTooltip && showErrors && errorsInfo}
{this.showMaxLenIndicator && (

View File

@ -39,13 +39,13 @@ export const isNumber: InputValidator = {
export const isUrl: InputValidator = {
condition: ({ type }) => type === "url",
message: () => _i18n._(t`Wrong url format`),
validate: value => !!value.match(/^$|^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/),
validate: value => !!value.match(/^http(s)?:\/\/\w+(\.\w+)*(:[0-9]+)?\/?(\/[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]*)*$/),
};
export const isPath: InputValidator = {
condition: ({ type }) => type === "text",
message: () => _i18n._(t`This field must be a valid path`),
validate: value => !value || fse.pathExistsSync(value),
validate: value => value && fse.pathExistsSync(value),
};
export const minLength: InputValidator = {

View File

@ -10,13 +10,15 @@ import { Input, InputProps } from "./input";
interface Props extends InputProps {
compact?: boolean; // show only search-icon when not focused
closeIcon?: boolean;
onClear?: () => void;
bindGlobalFocusHotkey?: boolean;
showClearIcon?: boolean;
onClear?(): void;
}
const defaultProps: Partial<Props> = {
autoFocus: true,
closeIcon: true,
bindGlobalFocusHotkey: true,
showClearIcon: true,
get placeholder() {
return _i18n._(t`Search...`);
},
@ -26,27 +28,27 @@ const defaultProps: Partial<Props> = {
export class SearchInput extends React.Component<Props> {
static defaultProps = defaultProps as object;
private input = createRef<Input>();
private inputRef = createRef<Input>();
componentDidMount() {
addEventListener("keydown", this.focus);
if (!this.props.bindGlobalFocusHotkey) return;
window.addEventListener("keydown", this.onGlobalKey);
}
componentWillUnmount() {
removeEventListener("keydown", this.focus);
window.removeEventListener("keydown", this.onGlobalKey);
}
clear = () => {
if (this.props.onClear) {
this.props.onClear();
@autobind()
onGlobalKey(evt: KeyboardEvent) {
const meta = evt.metaKey || evt.ctrlKey;
if (meta && evt.key === "f") {
this.inputRef.current.focus();
}
};
}
onChange = (val: string, evt: React.ChangeEvent<any>) => {
this.props.onChange(val, evt);
};
onKeyDown = (evt: React.KeyboardEvent<any>) => {
@autobind()
onKeyDown(evt: React.KeyboardEvent<any>) {
if (this.props.onKeyDown) {
this.props.onKeyDown(evt);
}
@ -56,29 +58,31 @@ export class SearchInput extends React.Component<Props> {
this.clear();
evt.stopPropagation();
}
};
}
@autobind()
focus(evt: KeyboardEvent) {
const meta = evt.metaKey || evt.ctrlKey;
if (meta && evt.key == "f") {
this.input.current.focus();
clear() {
if (this.props.onClear) {
this.props.onClear();
} else {
this.inputRef.current.setValue("");
}
}
render() {
const { className, compact, closeIcon, onClear, ...inputProps } = this.props;
const icon = this.props.value
? closeIcon ? <Icon small material="close" onClick={this.clear}/> : null
: <Icon small material="search"/>;
const { className, compact, onClear, showClearIcon, bindGlobalFocusHotkey, value, ...inputProps } = this.props;
let rightIcon = <Icon small material="search"/>;
if (showClearIcon && value) {
rightIcon = <Icon small material="close" onClick={this.clear}/>;
}
return (
<Input
{...inputProps}
className={cssNames("SearchInput", className, { compact })}
onChange={this.onChange}
value={value}
onKeyDown={this.onKeyDown}
iconRight={icon}
ref={this.input}
iconRight={rightIcon}
ref={this.inputRef}
/>
);
}

View File

@ -1724,11 +1724,6 @@
"@babel/runtime" "^7.11.2"
"@testing-library/dom" "^7.26.0"
"@tokenizer/token@^0.1.0", "@tokenizer/token@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.1.1.tgz#f0d92c12f87079ddfd1b29f614758b9696bc29e3"
integrity sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w==
"@types/anymatch@*":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
@ -6281,16 +6276,6 @@ file-loader@^6.0.0:
loader-utils "^2.0.0"
schema-utils "^2.6.5"
file-type@^14.7.1:
version "14.7.1"
resolved "https://registry.yarnpkg.com/file-type/-/file-type-14.7.1.tgz#f748732b3e70478bff530e1cf0ec2fe33608b1bb"
integrity sha512-sXAMgFk67fQLcetXustxfKX+PZgHIUFn96Xld9uH8aXPdX3xOp0/jg9OdouVTvQrf7mrn+wAa4jN/y9fUOOiRA==
dependencies:
readable-web-to-node-stream "^2.0.0"
strtok3 "^6.0.3"
token-types "^2.0.0"
typedarray-to-buffer "^3.1.5"
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@ -7409,7 +7394,7 @@ identity-obj-proxy@^3.0.0:
dependencies:
harmony-reflect "^1.4.6"
ieee754@^1.1.13, ieee754@^1.1.4:
ieee754@^1.1.4:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
@ -11272,11 +11257,6 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
peek-readable@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-3.1.0.tgz#250b08b7de09db8573d7fd8ea475215bbff14348"
integrity sha512-KGuODSTV6hcgdZvDrIDBUkN0utcAVj1LL7FfGbM0viKTtCHmtZcuEJ+lGqsp0fTFkGqesdtemV2yUSMeyy3ddA==
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
@ -12119,11 +12099,6 @@ readable-stream@~1.1.10:
isarray "0.0.1"
string_decoder "~0.10.x"
readable-web-to-node-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-2.0.0.tgz#751e632f466552ac0d5c440cc01470352f93c4b7"
integrity sha512-+oZJurc4hXpaaqsN68GoZGQAQIA3qr09Or4fqEsargABnbe5Aau8hFn6ISVleT3cpY/0n/8drn7huyyEvTbghA==
readdir-scoped-modules@^1.0.0, readdir-scoped-modules@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309"
@ -13581,15 +13556,6 @@ strip-outer@^1.0.1:
dependencies:
escape-string-regexp "^1.0.2"
strtok3@^6.0.3:
version "6.0.4"
resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.0.4.tgz#ede0d20fde5aa9fda56417c3558eaafccc724694"
integrity sha512-rqWMKwsbN9APU47bQTMEYTPcwdpKDtmf1jVhHzNW2cL1WqAxaM9iBb9t5P2fj+RV2YsErUWgQzHD5JwV0uCTEQ==
dependencies:
"@tokenizer/token" "^0.1.1"
"@types/debug" "^4.1.5"
peek-readable "^3.1.0"
style-loader@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a"
@ -13979,14 +13945,6 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
token-types@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/token-types/-/token-types-2.0.0.tgz#b23618af744818299c6fbf125e0fdad98bab7e85"
integrity sha512-WWvu8sGK8/ZmGusekZJJ5NM6rRVTTDO7/bahz4NGiSDb/XsmdYBn6a1N/bymUHuWYTWeuLUg98wUzvE4jPdCZw==
dependencies:
"@tokenizer/token" "^0.1.0"
ieee754 "^1.1.13"
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"