mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
fixes & refactoring
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
8b6616a591
commit
5fc0c639a1
@ -6,18 +6,18 @@ pr:
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- '*'
|
||||
- '*'
|
||||
tags:
|
||||
include:
|
||||
- "*"
|
||||
- "*"
|
||||
jobs:
|
||||
- job: Windows
|
||||
pool:
|
||||
vmImage: windows-2019
|
||||
strategy:
|
||||
matrix:
|
||||
node_14.x:
|
||||
node_version: 14.x
|
||||
node_12.x:
|
||||
node_version: 12.x
|
||||
steps:
|
||||
- powershell: |
|
||||
$CI_BUILD_TAG = git describe --tags
|
||||
@ -58,8 +58,8 @@ jobs:
|
||||
vmImage: macOS-10.14
|
||||
strategy:
|
||||
matrix:
|
||||
node_14.x:
|
||||
node_version: 14.x
|
||||
node_12.x:
|
||||
node_version: 12.x
|
||||
steps:
|
||||
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
||||
displayName: Set the tag name as an environment variable
|
||||
@ -104,8 +104,8 @@ jobs:
|
||||
vmImage: ubuntu-16.04
|
||||
strategy:
|
||||
matrix:
|
||||
node_14.x:
|
||||
node_version: 14.x
|
||||
node_12.x:
|
||||
node_version: 12.x
|
||||
steps:
|
||||
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
||||
displayName: Set the tag name as an environment variable
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
"bundledHelmVersion": "3.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12 <=14"
|
||||
"node": ">=12 <13"
|
||||
},
|
||||
"lingui": {
|
||||
"locales": [
|
||||
|
||||
@ -11,24 +11,23 @@ export interface DownloadFileTicket {
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export function downloadFile(opts: DownloadFileOptions): DownloadFileTicket {
|
||||
const { url, gzip = true } = opts;
|
||||
export function downloadFile({ url, gzip = true }: DownloadFileOptions): DownloadFileTicket {
|
||||
const fileChunks: Buffer[] = [];
|
||||
const req = request(url, { gzip });
|
||||
const promise: Promise<Buffer> = new Promise((resolve, reject) => {
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
fileChunks.push(chunk);
|
||||
});
|
||||
req.on("complete", () => {
|
||||
resolve(Buffer.concat(fileChunks));
|
||||
});
|
||||
req.on("error", err => {
|
||||
req.once("error", err => {
|
||||
reject({ url, err });
|
||||
});
|
||||
req.once("complete", () => {
|
||||
resolve(Buffer.concat(fileChunks));
|
||||
});
|
||||
});
|
||||
return {
|
||||
url: url,
|
||||
promise: promise,
|
||||
url,
|
||||
promise,
|
||||
cancel() {
|
||||
req.abort();
|
||||
}
|
||||
|
||||
@ -25,10 +25,10 @@ export function readFileFromTar(tarFilePath: string, opts: ReadFileFromTarOpts):
|
||||
entry.on("data", chunk => {
|
||||
fileChunks.push(chunk);
|
||||
});
|
||||
entry.on("error", err => {
|
||||
entry.once("error", err => {
|
||||
reject(`Reading ${entry.path} error: ${err}`);
|
||||
});
|
||||
entry.on("end", () => {
|
||||
entry.once("end", () => {
|
||||
resolve(Buffer.concat(fileChunks));
|
||||
});
|
||||
},
|
||||
|
||||
@ -41,3 +41,5 @@ export const apiKubePrefix = "/api-kube"; // k8s cluster apis
|
||||
// Links
|
||||
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues";
|
||||
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI";
|
||||
export const docsUrl = "https://docs.k8slens.dev/";
|
||||
export const supportUrl = "https://docs.k8slens.dev/latest/support/";
|
||||
|
||||
@ -97,15 +97,6 @@ export class ExtensionManager {
|
||||
}
|
||||
}
|
||||
|
||||
getNpmPackageTarballUrl(packageName: string) {
|
||||
try {
|
||||
const command = [this.npmPath, "view", packageName, "dist.tarball", "--silent"];
|
||||
return child_process.execSync(command.join(" "), { encoding: "utf8" }).trim();
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected installPackages(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { app, BrowserWindow, dialog, ipcMain, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
|
||||
import { autorun } from "mobx";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { appName, isMac, isWindows, isTestEnv } from "../common/vars";
|
||||
import { appName, isMac, isWindows, isTestEnv, docsUrl, supportUrl } from "../common/vars";
|
||||
import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route";
|
||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
||||
@ -24,6 +24,7 @@ export function showAbout(browserWindow: BrowserWindow) {
|
||||
`${appName}: ${app.getVersion()}`,
|
||||
`Electron: ${process.versions.electron}`,
|
||||
`Chrome: ${process.versions.chrome}`,
|
||||
`Node: ${process.versions.node}`,
|
||||
`Copyright 2020 Mirantis, Inc.`,
|
||||
];
|
||||
dialog.showMessageBoxSync(browserWindow, {
|
||||
@ -215,13 +216,13 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
{
|
||||
label: "Documentation",
|
||||
click: async () => {
|
||||
shell.openExternal('https://docs.k8slens.dev/');
|
||||
shell.openExternal(docsUrl);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Support",
|
||||
click: async () => {
|
||||
shell.openExternal('https://docs.k8slens.dev/latest/support/');
|
||||
shell.openExternal(supportUrl);
|
||||
},
|
||||
},
|
||||
...ignoreOnMac([
|
||||
|
||||
@ -22,16 +22,6 @@
|
||||
> .flex.gaps {
|
||||
--flex-gap: #{$padding};
|
||||
}
|
||||
|
||||
.install-extension {
|
||||
.Clipboard {
|
||||
font-size: $font-size-small;
|
||||
|
||||
&:hover {
|
||||
color: $textColorSecondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.extensions-path {
|
||||
@ -43,10 +33,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.Clipboard {
|
||||
display: inline;
|
||||
vertical-align: baseline;
|
||||
font-size: $font-size-small;
|
||||
|
||||
&:hover {
|
||||
color: $textColorSecondary;
|
||||
}
|
||||
}
|
||||
|
||||
.SearchInput {
|
||||
--spacing: #{$padding};
|
||||
}
|
||||
|
||||
.SubTitle {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.WizardLayout {
|
||||
padding: 0;
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import { Button } from "../button";
|
||||
import { WizardLayout } from "../layout/wizard-layout";
|
||||
import { DropFileInput, Input, InputValidators, SearchInput } from "../input";
|
||||
import { Icon } from "../icon";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { PageLayout } from "../layout/page-layout";
|
||||
import { Clipboard } from "../clipboard";
|
||||
import logger from "../../../main/logger";
|
||||
@ -21,6 +22,7 @@ import { LensExtensionManifest, sanitizeExtensionName } from "../../../extension
|
||||
import { Notifications } from "../notifications";
|
||||
import { downloadFile } from "../../../common/utils";
|
||||
import { extractTar, readFileFromTar } from "../../../common/utils/tar";
|
||||
import { docsUrl } from "../../../common/vars";
|
||||
|
||||
interface InstallRequest {
|
||||
fileName: string;
|
||||
@ -28,7 +30,11 @@ interface InstallRequest {
|
||||
data?: Buffer;
|
||||
}
|
||||
|
||||
interface InstallRequestValidated extends InstallRequest {
|
||||
interface InstallRequestPreloaded extends InstallRequest {
|
||||
data: Buffer;
|
||||
}
|
||||
|
||||
interface InstallRequestValidated extends InstallRequestPreloaded {
|
||||
manifest: LensExtensionManifest;
|
||||
tmpFile: string; // temp file for unpacking
|
||||
}
|
||||
@ -54,6 +60,10 @@ export class Extensions extends React.Component {
|
||||
return extensionManager.localFolderPath;
|
||||
}
|
||||
|
||||
getExtensionPackageTemp(fileName = "") {
|
||||
return path.join(os.tmpdir(), "lens-extensions", fileName);
|
||||
}
|
||||
|
||||
getExtensionDestFolder(name: string) {
|
||||
return path.join(this.extensionsPath, sanitizeExtensionName(name));
|
||||
}
|
||||
@ -79,24 +89,16 @@ export class Extensions extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
installExtensions = () => {
|
||||
if (this.downloadUrl) {
|
||||
this.installFromNpmOrUrl(this.downloadUrl);
|
||||
this.downloadUrl = "";
|
||||
addExtensions = () => {
|
||||
const { downloadUrl } = this;
|
||||
if (downloadUrl && InputValidators.isUrl.validate(downloadUrl)) {
|
||||
this.installFromUrl(downloadUrl);
|
||||
} else {
|
||||
this.installFromSelectFileDialog();
|
||||
}
|
||||
};
|
||||
|
||||
installFromNpmOrUrl = async (url = this.downloadUrl) => {
|
||||
if (!InputValidators.isUrl.validate(url)) {
|
||||
const npmPackageName = url;
|
||||
url = extensionManager.getNpmPackageTarballUrl(npmPackageName);
|
||||
if (!url) {
|
||||
Notifications.error(`Error: npm package "${npmPackageName}" not found!`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
installFromUrl = async (url: string) => {
|
||||
try {
|
||||
const { promise: filePromise } = downloadFile({ url });
|
||||
this.requestInstall([{
|
||||
@ -105,10 +107,7 @@ export class Extensions extends React.Component {
|
||||
}]);
|
||||
} catch (err) {
|
||||
Notifications.error(
|
||||
<div className="flex column gaps">
|
||||
<p>Installation from URL has failed: <b>{String(err)}</b></p>
|
||||
<p>URL: <em>{url}</em></p>
|
||||
</div>
|
||||
<p>Installation via URL has failed: <b>{String(err)}</b></p>
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -123,89 +122,110 @@ export class Extensions extends React.Component {
|
||||
);
|
||||
};
|
||||
|
||||
async requestInstall(installRequests: InstallRequest[]) {
|
||||
const pendingFiles: Promise<any>[] = [];
|
||||
async preloadExtensions(requests: InstallRequest[], { showError = true } = {}) {
|
||||
const preloadedRequests = requests.filter(req => req.data);
|
||||
await Promise.all(
|
||||
requests
|
||||
.filter(req => !req.data && req.filePath)
|
||||
.map(req => {
|
||||
return fse.readFile(req.filePath).then(data => {
|
||||
req.data = data;
|
||||
preloadedRequests.push(req);
|
||||
}).catch(err => {
|
||||
if (showError) {
|
||||
Notifications.error(`Error while reading "${req.filePath}": ${String(err)}`);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
return preloadedRequests as InstallRequestPreloaded[];
|
||||
}
|
||||
|
||||
// read extensions with provided system path if any
|
||||
installRequests.forEach(ext => {
|
||||
if (ext.data) return;
|
||||
const promise = fse.readFile(ext.filePath)
|
||||
.then(data => ext.data = data)
|
||||
.catch(err => {
|
||||
Notifications.error(`Error while reading "${ext.filePath}": ${String(err)}`);
|
||||
});
|
||||
pendingFiles.push(promise);
|
||||
async validatePackage(filePath: string): Promise<LensExtensionManifest> {
|
||||
const packageJson: Buffer = await readFileFromTar(filePath, {
|
||||
// tarball from npm contains single root folder "package/*"
|
||||
fileMatcher: (path: string) => !!path.match(/(\w+\/)?package\.json$/),
|
||||
notFoundMessage: "Invalid extension, package.json not found",
|
||||
});
|
||||
await Promise.all(pendingFiles);
|
||||
installRequests = installRequests.filter(item => item.data); // remove items with reading errors
|
||||
const manifest: LensExtensionManifest = JSON.parse(packageJson.toString("utf8"));
|
||||
if (!manifest.lens && !manifest.renderer) {
|
||||
throw `package.json must specify "main" and/or "renderer" fields`;
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
// prepare temp folder
|
||||
const tempFolder = path.join(os.tmpdir(), "lens-extensions");
|
||||
await fse.ensureDir(tempFolder);
|
||||
async createTempFilesAndValidate(requests: InstallRequestPreloaded[], { showErrors = true } = {}) {
|
||||
const validatedRequests: InstallRequestValidated[] = [];
|
||||
|
||||
// copy files to temp, get extension info from package.json and do basic validation
|
||||
const validatedInstalls: Promise<InstallRequestValidated>[] = installRequests.map(async installReq => {
|
||||
const { fileName, data } = installReq;
|
||||
const tempFile = path.join(tempFolder, fileName);
|
||||
await fse.writeFileSync(tempFile, data); // copy to temp
|
||||
try {
|
||||
const packageJson: Buffer = await readFileFromTar(tempFile, {
|
||||
// tarball from npm contains single root folder "package/*"
|
||||
fileMatcher: (path: string) => !!path.match(/(\w+\/)?package\.json$/),
|
||||
notFoundMessage: "Extension's manifest file (package.json) not found",
|
||||
});
|
||||
const manifest: LensExtensionManifest = JSON.parse(packageJson.toString("utf8"));
|
||||
if (!manifest.lens && !manifest.renderer) {
|
||||
throw `package.json must specify "main" and/or "renderer" fields`;
|
||||
// copy files to temp
|
||||
await fse.ensureDir(this.getExtensionPackageTemp());
|
||||
requests.forEach(req => {
|
||||
const tempFile = this.getExtensionPackageTemp(req.fileName);
|
||||
fse.writeFileSync(tempFile, req.data);
|
||||
});
|
||||
|
||||
// validate packages
|
||||
await Promise.all(
|
||||
requests.map(async req => {
|
||||
const tempFile = this.getExtensionPackageTemp(req.fileName);
|
||||
try {
|
||||
const manifest = await this.validatePackage(tempFile);
|
||||
validatedRequests.push({
|
||||
...req,
|
||||
manifest: manifest,
|
||||
tmpFile: tempFile,
|
||||
});
|
||||
} catch (err) {
|
||||
fse.unlink(tempFile).catch(() => null); // remove invalid temp package
|
||||
if (showErrors) {
|
||||
Notifications.error(
|
||||
<div className="flex column gaps">
|
||||
<p>Installing <em>{req.fileName}</em> has failed, skipping.</p>
|
||||
<p>Reason: <em>{String(err)}</em></p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
...installReq,
|
||||
manifest: manifest,
|
||||
tmpFile: tempFile,
|
||||
};
|
||||
} catch (err) {
|
||||
fse.unlink(tempFile).catch(() => null); // remove invalid temp file
|
||||
Notifications.error(
|
||||
<div className="flex column gaps">
|
||||
<p>Installing <em>{fileName}</em> has failed, skipping.</p>
|
||||
<p>Reason: <em>{String(err)}</em></p>
|
||||
})
|
||||
);
|
||||
return validatedRequests;
|
||||
}
|
||||
|
||||
async requestInstall(requests: InstallRequest[]) {
|
||||
const preloadedRequests = await this.preloadExtensions(requests);
|
||||
const validatedRequests = await this.createTempFilesAndValidate(preloadedRequests);
|
||||
|
||||
validatedRequests.forEach(install => {
|
||||
const { name, version, description } = install.manifest;
|
||||
const extensionFolder = this.getExtensionDestFolder(name);
|
||||
const folderExists = fse.existsSync(extensionFolder);
|
||||
if (!folderExists) {
|
||||
// auto-install extension if not yet exists
|
||||
this.unpackExtension(install);
|
||||
} else {
|
||||
// otherwise confirmation required (re-install / update)
|
||||
const removeNotification = Notifications.info(
|
||||
<div className="InstallingExtensionNotification flex gaps align-center">
|
||||
<div className="flex column gaps">
|
||||
<p>Install extension <b>{name}@{version}</b>?</p>
|
||||
<p>Description: <em>{description}</em></p>
|
||||
<div className="remove-folder-warning" onClick={() => shell.openPath(extensionFolder)}>
|
||||
<b>Warning:</b> <code>{extensionFolder}</code> will be removed before installation.
|
||||
</div>
|
||||
</div>
|
||||
<Button autoFocus label="Install" onClick={() => {
|
||||
removeNotification();
|
||||
this.unpackExtension(install);
|
||||
}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// final step, provide UI with extension info for reviewing and confirming installation
|
||||
const extensions = await Promise.all(validatedInstalls);
|
||||
extensions.forEach(install => {
|
||||
if (!install) {
|
||||
return; // skip validating errors if any
|
||||
}
|
||||
const { fileName, manifest } = install;
|
||||
const { name, version, description } = manifest;
|
||||
const extensionFolder = this.getExtensionDestFolder(name);
|
||||
const folderExists = fse.existsSync(extensionFolder);
|
||||
const removeNotification = Notifications.info(
|
||||
<div className="InstallingExtensionNotification flex gaps align-center">
|
||||
<div className="flex column gaps">
|
||||
<p>Install extension <b title={fileName}>{name}@{version}</b>?</p>
|
||||
<p>Description: <em>{description}</em></p>
|
||||
{folderExists && (
|
||||
<div className="remove-folder-warning" onClick={() => shell.openPath(extensionFolder)}>
|
||||
<b>Warning:</b> <code>{extensionFolder}</code> will be removed before installation.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button autoFocus label="Install" onClick={() => {
|
||||
removeNotification();
|
||||
this.unpackExtension(install);
|
||||
}}/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async unpackExtension({ fileName, tmpFile, manifest: { name, version } }: InstallRequestValidated) {
|
||||
logger.info(`Unpacking extension ${name} from ${fileName}`);
|
||||
const extName = `${name}@${version}`;
|
||||
logger.info(`Unpacking extension ${extName}`, { fileName, tmpFile });
|
||||
const unpackingTempFolder = path.join(path.dirname(tmpFile), path.basename(tmpFile) + "-unpacked");
|
||||
const extensionFolder = this.getExtensionDestFolder(name);
|
||||
try {
|
||||
@ -218,18 +238,18 @@ export class Extensions extends React.Component {
|
||||
const unpackedFiles = await fse.readdir(unpackingTempFolder);
|
||||
let unpackedRootFolder = unpackingTempFolder;
|
||||
if (unpackedFiles.length === 1) {
|
||||
// handle case when extension.tgz packed with top root folder,
|
||||
// check if %extension.tgz was packed with single top folder,
|
||||
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
|
||||
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);
|
||||
}
|
||||
await fse.ensureDir(extensionFolder);
|
||||
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
||||
Notifications.ok(
|
||||
<p>Extension <b>{name}/{version}</b> successfully installed!</p>
|
||||
<p>Extension <b>{extName}</b> successfully installed!</p>
|
||||
);
|
||||
} catch (err) {
|
||||
Notifications.error(
|
||||
<p>Installing extension <b>{name}</b> has failed: <em>{err}</em></p>
|
||||
<p>Installing extension <b>{extName}</b> has failed: <em>{err}</em></p>
|
||||
);
|
||||
} finally {
|
||||
// clean up
|
||||
@ -247,34 +267,38 @@ export class Extensions extends React.Component {
|
||||
features of Lens are built as extensions and use the same Extension API.
|
||||
</div>
|
||||
<div>
|
||||
<p><em>Extensions loaded from:</em></p>
|
||||
<SubTitle title="Extensions loaded from:"/>
|
||||
<div className="extensions-path flex inline" onClick={() => shell.openPath(this.extensionsPath)}>
|
||||
<Icon material="folder" tooltip={{ children: "Open folder", preferredPositions: "bottom" }}/>
|
||||
<code>{this.extensionsPath}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="install-extension flex column gaps">
|
||||
<em>
|
||||
Install extensions from tarball ({this.supportedFormats.join(", ")}):
|
||||
</em>
|
||||
<SubTitle title="Install extensions:"/>
|
||||
<Input
|
||||
showErrorsAsTooltip={true}
|
||||
className="box grow"
|
||||
theme="round-black"
|
||||
placeholder="URL or npm-package-name"
|
||||
iconLeft="link"
|
||||
placeholder={`URL to packed extension (${this.supportedFormats.join(", ")})`}
|
||||
validators={InputValidators.isUrl}
|
||||
value={this.downloadUrl}
|
||||
onChange={v => this.downloadUrl = v}
|
||||
onSubmit={this.installExtensions}
|
||||
onSubmit={this.addExtensions}
|
||||
/>
|
||||
<Button
|
||||
primary
|
||||
label="Add extensions"
|
||||
onClick={this.installExtensions}
|
||||
onClick={this.addExtensions}
|
||||
/>
|
||||
<p className="hint">
|
||||
<Trans><b>Pro-Tip 1</b>: you can download packed extension from NPM via</Trans>
|
||||
<Trans><b>Pro-Tip 1</b>: you can obtain package tarball from NPM via</Trans>{" "}
|
||||
<Clipboard showNotification>
|
||||
<code>npm pack %package-name</code>
|
||||
<code>npm view %package dist.tarball</code>
|
||||
</Clipboard>
|
||||
<span> or download package first with </span>
|
||||
<Clipboard showNotification>
|
||||
<code>npm pack %package</code>
|
||||
</Clipboard>
|
||||
</p>
|
||||
<p className="hint">
|
||||
@ -284,7 +308,7 @@ export class Extensions extends React.Component {
|
||||
<div className="more-info flex inline gaps align-center">
|
||||
<Icon material="local_fire_department"/>
|
||||
<p>
|
||||
Check out documentation to <a href="https://docs.k8slens.dev/" target="_blank">learn more</a>
|
||||
Check out documentation to <a href={docsUrl} target="_blank">learn more</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -88,12 +88,11 @@
|
||||
//- Themes
|
||||
|
||||
&.theme {
|
||||
&.invalid {
|
||||
box-shadow: 0 0 0 2px $colorError;
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
&.round-black {
|
||||
&.invalid label {
|
||||
border-color: $colorSoftError !important;
|
||||
}
|
||||
|
||||
label {
|
||||
background: $mainBackground;
|
||||
border: 1px solid $borderFaintColor;
|
||||
@ -115,6 +114,6 @@
|
||||
|
||||
.Tooltip.InputTooltipError {
|
||||
--bgc: #{$colorError};
|
||||
--border: 1px solid $colorSoftError;
|
||||
--border: none;
|
||||
--color: white;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user