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

Merge remote-tracking branch 'origin/master' into monaco_editor_refactoring

# Conflicts:
#	src/renderer/components/+workloads-pods/pod-details-affinities.tsx
#	src/renderer/components/dock/create-resource.tsx
#	src/renderer/components/dock/edit-resource.tsx
#	src/renderer/components/dock/editor-panel.tsx
This commit is contained in:
Roman 2021-10-19 13:32:17 +03:00
commit d6b7001f72
28 changed files with 348 additions and 161 deletions

View File

@ -206,7 +206,7 @@
"http-proxy": "^1.18.1",
"immer": "^9.0.6",
"joi": "^17.4.2",
"js-yaml": "^3.14.0",
"js-yaml": "^4.1.0",
"jsdom": "^16.7.0",
"jsonpath": "^1.1.1",
"lodash": "^4.17.15",
@ -267,7 +267,7 @@
"@types/chart.js": "^2.9.34",
"@types/color": "^3.0.2",
"@types/crypto-js": "^3.1.47",
"@types/dompurify": "^2.0.2",
"@types/dompurify": "^2.3.1",
"@types/electron-devtools-installer": "^2.2.0",
"@types/fs-extra": "^9.0.1",
"@types/glob-to-regexp": "^0.4.1",
@ -276,7 +276,7 @@
"@types/html-webpack-plugin": "^3.2.6",
"@types/http-proxy": "^1.17.7",
"@types/jest": "^26.0.24",
"@types/js-yaml": "^3.12.4",
"@types/js-yaml": "^4.0.2",
"@types/jsdom": "^16.2.13",
"@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.155",
@ -346,7 +346,7 @@
"jest": "26.6.3",
"jest-canvas-mock": "^2.3.1",
"jest-fetch-mock": "^3.0.3",
"jest-mock-extended": "^1.0.16",
"jest-mock-extended": "^1.0.18",
"make-plural": "^6.2.2",
"mini-css-extract-plugin": "^1.6.2",
"node-gyp": "7.1.2",
@ -361,7 +361,7 @@
"raw-loader": "^4.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-refresh": "^0.9.0",
"react-router-dom": "^5.2.0",
"react-router-dom": "^5.3.0",
"react-select": "3.2.0",
"react-select-event": "^5.1.0",
"react-table": "^7.7.0",

View File

@ -426,7 +426,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
it("replaces array format access token and expiry into string", async () => {
const file = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
const config = fs.readFileSync(file, "utf8");
const kc = yaml.safeLoad(config);
const kc = yaml.load(config) as Record<string, any>;
expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string");
expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string");

View File

@ -324,6 +324,15 @@ describe("HotbarStore", () => {
console.error = error;
console.warn = warn;
});
it("checks if entity already pinned to hotbar", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
expect(hotbarStore.isAddedToActive(testCluster)).toBeTruthy();
expect(hotbarStore.isAddedToActive(awsCluster)).toBeFalsy();
});
});
describe("pre beta-5 migrations", () => {

View File

@ -35,8 +35,11 @@ export type WebLinkSpec = {
};
export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus, WebLinkSpec> {
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public readonly kind = "WebLink";
public static readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public static readonly kind = "WebLink";
public readonly apiVersion = WebLink.apiVersion;
public readonly kind = WebLink.kind;
async onRun() {
window.open(this.spec.url, "_blank");

View File

@ -171,7 +171,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}};
if (hotbar.items.find(i => i?.entity.uid === uid)) {
if (this.isAddedToActive(item)) {
return;
}
@ -274,6 +274,14 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id;
}
/**
* Checks if entity already pinned to hotbar
* @returns boolean
*/
isAddedToActive(entity: CatalogEntity) {
return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid);
}
}
/**

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import jsYaml from "js-yaml";
import yaml from "js-yaml";
import { autoBind, formatDuration } from "../../utils";
import capitalize from "lodash/capitalize";
import { apiBase } from "../index";
@ -113,21 +113,31 @@ export async function getRelease(name: string, namespace: string): Promise<IRele
}
export async function createRelease(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
const chart = `${repo}/${rawChart}`;
const values = yaml.load(rawValues);
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
return apiBase.post(endpoint(), { data });
return apiBase.post(endpoint(), {
data: {
chart,
values,
...data,
}
});
}
export async function updateRelease(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
const chart = `${repo}/${rawChart}`;
const values = yaml.load(rawValues);
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
return apiBase.put(endpoint({ name, namespace }), { data });
return apiBase.put(endpoint({ name, namespace }), {
data: {
chart,
values,
...data,
}
});
}
export async function deleteRelease(name: string, namespace: string): Promise<JsonApiData> {

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import jsYaml from "js-yaml";
import yaml from "js-yaml";
import type { KubeJsonApiData } from "../kube-json-api";
import { apiBase } from "../index";
import type { Patch } from "rfc6902";
@ -30,7 +30,13 @@ export const annotations = [
export async function update(resource: object | string): Promise<KubeJsonApiData> {
if (typeof resource === "string") {
resource = jsYaml.safeLoad(resource);
const parsed = yaml.load(resource);
if (typeof parsed !== "object") {
throw new Error("Cannot update resource to string or number");
}
resource = parsed;
}
return apiBase.post<KubeJsonApiData>("/stack", { data: resource });

View File

@ -133,7 +133,7 @@ export class ResourceStack {
if (!resourceData.trim()) continue;
const resourceArray = yaml.safeLoadAll(resourceData.toString());
const resourceArray = yaml.loadAll(resourceData.toString());
resourceArray.forEach((resource) => {
if (resource?.metadata) {
@ -143,7 +143,7 @@ export class ResourceStack {
resource.metadata.labels["app.kubernetes.io/created-by"] = "resource-stack";
}
resources.push(yaml.safeDump(resource));
resources.push(yaml.dump(resource));
});
}

View File

@ -111,7 +111,7 @@ interface OptionsResult {
}
function loadToOptions(rawYaml: string): OptionsResult {
const parsed = yaml.safeLoad(rawYaml);
const parsed = yaml.load(rawYaml);
const { error } = kubeConfigSchema.validate(parsed, {
abortEarly: false,
allowUnknown: true,
@ -248,7 +248,7 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
logger.debug("Dumping KubeConfig:", config);
// skipInvalid: true makes dump ignore undefined values
return yaml.safeDump(config, { skipInvalid: true });
return yaml.dump(config, { skipInvalid: true });
}
/**

View File

@ -29,6 +29,11 @@ import { helmCli } from "./helm-cli";
import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api";
import { sortCharts } from "../../common/utils";
export interface HelmCacheFile {
apiVersion: string;
entries: RepoHelmChartList;
}
export class HelmChartManager {
static #cache = new Map<string, Buffer>();
@ -82,7 +87,11 @@ export class HelmChartManager {
protected async cachedYaml(): Promise<RepoHelmChartList> {
if (!HelmChartManager.#cache.has(this.repo.name)) {
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8");
const { entries } = yaml.safeLoad(cacheFile) as { entries: RepoHelmChartList };
const data = yaml.load(cacheFile) as string | number | HelmCacheFile;
if (typeof data !== "object" || !data) {
return {};
}
/**
* Do some initial preprocessing on the data, so as to avoid needing to do it later
@ -92,7 +101,7 @@ export class HelmChartManager {
*/
const normalized = Object.fromEntries(
Object.entries(entries)
Object.entries(data.entries)
.map(([name, charts]) => [
name,
sortCharts(

View File

@ -53,7 +53,7 @@ export async function installChart(chart: string, values: any, name: string | un
const helm = await helmCli.binaryPath();
const fileName = tempy.file({ name: "values.yaml" });
await fse.writeFile(fileName, yaml.safeDump(values));
await fse.writeFile(fileName, yaml.dump(values));
try {
let generateName = "";
@ -83,7 +83,7 @@ export async function upgradeRelease(name: string, chart: string, values: any, n
const helm = await helmCli.binaryPath();
const fileName = tempy.file({ name: "values.yaml" });
await fse.writeFile(fileName, yaml.safeDump(values));
await fse.writeFile(fileName, yaml.dump(values));
try {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();

View File

@ -101,18 +101,28 @@ export class HelmRepoManager extends Singleton {
return repos.find(repo => repo.name === name);
}
private async readConfig(): Promise<HelmRepoConfig> {
try {
const rawConfig = await readFile(this.helmEnv.HELM_REPOSITORY_CONFIG, "utf8");
const parsedConfig = yaml.load(rawConfig);
if (typeof parsedConfig === "object" && parsedConfig) {
return parsedConfig as HelmRepoConfig;
}
} catch { }
return {
repositories: []
};
}
public async repositories(): Promise<HelmRepo[]> {
try {
if (!this.initialized) {
await this.init();
}
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8")
.then((yamlContent: string) => yaml.safeLoad(yamlContent))
.catch(() => ({
repositories: []
}));
const { repositories } = await this.readConfig();
if (!repositories.length) {
await HelmRepoManager.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" });

View File

@ -78,7 +78,7 @@ export class ResourceApplier {
resource = this.sanitizeObject(resource);
appEventBus.emit({ name: "resource", action: "apply" });
return this.kubectlApply(yaml.safeDump(resource));
return this.kubectlApply(yaml.dump(resource));
}
protected async kubectlApply(content: string): Promise<string> {

View File

@ -32,9 +32,13 @@ export default {
const cluster = value[1];
if (!cluster.kubeConfig) continue;
const kubeConfig = yaml.safeLoad(cluster.kubeConfig);
const config = yaml.load(cluster.kubeConfig);
if (!kubeConfig.hasOwnProperty("users")) continue;
if (!config || typeof config !== "object" || !config.hasOwnProperty("users")) {
continue;
}
const kubeConfig = config as Record<string, any>;
const userObj = kubeConfig.users[0];
if (userObj) {
@ -56,7 +60,7 @@ export default {
name: userObj.name,
user
}];
cluster.kubeConfig = yaml.safeDump(kubeConfig);
cluster.kubeConfig = yaml.dump(kubeConfig);
store.set(clusterKey, cluster);
}
}

View File

@ -28,9 +28,9 @@ import { makeObservable, observable } from "mobx";
import { navigate } from "../../navigation";
import { MenuItem } from "../menu";
import { ConfirmDialog } from "../confirm-dialog";
import { HotbarStore } from "../../../common/hotbar-store";
import { Icon } from "../icon";
import type { CatalogEntityItem } from "./catalog-entity-item";
import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
export interface CatalogEntityDrawerMenuProps<T extends CatalogEntity> extends MenuActionsProps {
item: CatalogEntityItem<T> | null | undefined;
@ -70,10 +70,6 @@ export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Comp
}
}
addToHotbar(entity: CatalogEntity): void {
HotbarStore.getInstance().addToHotbar(entity);
}
getMenuItems(entity: T): React.ReactChild[] {
if (!entity) {
return [];
@ -99,9 +95,12 @@ export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Comp
}
items.push(
<MenuItem key="add-to-hotbar" onClick={() => this.addToHotbar(entity) }>
<Icon material="playlist_add" small tooltip="Add to Hotbar" />
</MenuItem>
<HotbarToggleMenuItem
key="hotbar-toggle"
entity={entity}
addContent={<Icon material="push_pin" small tooltip={"Add to Hotbar"}/>}
removeContent={<Icon svg="unpin" small tooltip={"Remove from Hotbar"}/>}
/>
);
return items;
@ -109,7 +108,7 @@ export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Comp
render() {
const { className, item: entity, ...menuProps } = this.props;
if (!this.contextMenu || !entity.enabled) {
return null;
}

View File

@ -19,10 +19,36 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.list :global(.TableRow) {
.entityName {
position: relative;
width: min-content;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 24px;
.pinIcon {
position: absolute;
right: 0;
opacity: 0;
transition: none;
&:hover {
/* Drop styles defined for <Icon/> */
background-color: transparent;
box-shadow: none;
}
}
}
&:hover .pinIcon {
opacity: 1;
}
}
.iconCell {
max-width: 40px;
display: flex;
align-items: center;
@apply flex items-center max-w-[40px];
}
.iconCell > div * {

View File

@ -46,6 +46,12 @@ jest.mock("@electron/remote", () => {
};
});
jest.mock("./hotbar-toggle-menu-item", () => {
return {
HotbarToggleMenuItem: () => <div>menu item</div>
};
});
class MockCatalogEntity extends CatalogEntity {
public apiVersion = "api";
public kind = "kind";

View File

@ -37,13 +37,15 @@ import { CatalogAddButton } from "./catalog-add-button";
import type { RouteComponentProps } from "react-router";
import { Notifications } from "../notifications";
import { MainLayout } from "../layout/main-layout";
import { createAppStorage, cssNames } from "../../utils";
import { createAppStorage, cssNames, prevDefault } from "../../utils";
import { makeCss } from "../../../common/utils/makeCss";
import { CatalogEntityDetails } from "./catalog-entity-details";
import { browseCatalogTab, catalogURL, CatalogViewRouteParam } from "../../../common/routes";
import { CatalogMenu } from "./catalog-menu";
import { HotbarIcon } from "../hotbar/hotbar-icon";
import { RenderDelay } from "../render-delay/render-delay";
import { Icon } from "../icon";
import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
export const previousActiveTab = createAppStorage("catalog-previous-active-tab", browseCatalogTab);
@ -129,6 +131,10 @@ export class Catalog extends React.Component<Props> {
HotbarStore.getInstance().addToHotbar(item.entity);
}
removeFromHotbar(item: CatalogEntityItem<CatalogEntity>): void {
HotbarStore.getInstance().removeFromHotbar(item.getId());
}
onDetails = (item: CatalogEntityItem<CatalogEntity>) => {
if (this.catalogEntityStore.selectedItemId) {
this.catalogEntityStore.selectedItemId = null;
@ -194,13 +200,34 @@ export class Catalog extends React.Component<Props> {
</MenuItem>
))
}
<MenuItem key="add-to-hotbar" onClick={() => this.addToHotbar(item)}>
Pin to Hotbar
</MenuItem>
<HotbarToggleMenuItem
key="hotbar-toggle"
entity={item.entity}
addContent="Add to Hotbar"
removeContent="Remove from Hotbar"
/>
</MenuActions>
);
};
renderName(item: CatalogEntityItem<CatalogEntity>) {
const isItemInHotbar = HotbarStore.getInstance().isAddedToActive(item.entity);
return (
<div className={styles.entityName}>
{item.name}
<Icon
small
className={styles.pinIcon}
material={!isItemInHotbar && "push_pin"}
svg={isItemInHotbar && "unpin"}
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
onClick={prevDefault(() => isItemInHotbar ? this.removeFromHotbar(item) : this.addToHotbar(item))}
/>
</div>
);
}
renderIcon(item: CatalogEntityItem<CatalogEntity>) {
return (
<RenderDelay>
@ -231,7 +258,7 @@ export class Catalog extends React.Component<Props> {
renderHeaderTitle={activeCategory?.metadata.name || "Browse All"}
isSelectable={false}
isConfigurable={true}
className="CatalogItemList"
className={styles.list}
store={this.catalogEntityStore}
sortingCallbacks={{
[sortBy.name]: item => item.name,
@ -255,7 +282,7 @@ export class Catalog extends React.Component<Props> {
})}
renderTableContents={item => [
this.renderIcon(item),
item.name,
this.renderName(item),
!activeCategory && item.kind,
item.source,
item.getLabelBadges(),

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React, { ReactNode, useState } from "react";
import { HotbarStore } from "../../../common/hotbar-store";
import { MenuItem } from "../menu";
import type { CatalogEntity } from "../../api/catalog-entity";
export function HotbarToggleMenuItem(props: { entity: CatalogEntity, addContent: ReactNode, removeContent: ReactNode }) {
const store = HotbarStore.getInstance(false);
const add = () => store.addToHotbar(props.entity);
const remove = () => store.removeFromHotbar(props.entity.getId());
const [itemInHotbar, setItemInHotbar] = useState(store.isAddedToActive(props.entity));
return (
<MenuItem onClick={() => {
itemInHotbar ? remove() : add();
setItemInHotbar(!itemInHotbar);
}}>
{itemInHotbar ? props.removeContent : props.addContent }
</MenuItem>
);
}

View File

@ -20,7 +20,7 @@
*/
import React from "react";
import jsYaml from "js-yaml";
import yaml from "js-yaml";
import { DrawerItem, DrawerParamToggler } from "../drawer";
import type { DaemonSet, Deployment, Job, Pod, ReplicaSet, StatefulSet } from "../../../common/k8s-api/endpoints";
import { MonacoEditor } from "../monaco-editor";
@ -41,7 +41,7 @@ export class PodDetailsAffinities extends React.Component<Props> {
<DrawerItem name="Affinities" className="PodDetailsAffinities">
<DrawerParamToggler label={affinitiesNum}>
<div style={{ height: 200 }}>
<MonacoEditor readOnly value={jsYaml.dump(affinities)}/>
<MonacoEditor readOnly value={yaml.dump(affinities)}/>
</div>
</DrawerParamToggler>
</DrawerItem>

View File

@ -21,13 +21,12 @@
import React from "react";
import { GroupSelectOption, Select, SelectOption } from "../select";
import jsYaml from "js-yaml";
import yaml from "js-yaml";
import { computed, makeObservable } from "mobx";
import { observer } from "mobx-react";
import { createResourceStore } from "./create-resource.store";
import { InfoPanel, InfoPanelProps } from "./info-panel";
import * as resourceApplierApi from "../../../common/k8s-api/endpoints/resource-applier.api";
import type { JsonApiErrorParsed } from "../../../common/k8s-api/json-api";
import { Notifications } from "../notifications";
import { TabKind } from "./dock.store";
import { dockViewsManager } from "./dock.views-manager";
@ -58,36 +57,38 @@ export class CreateResourceInfoPanel extends React.Component<Props> {
return createResourceStore.getData(this.tabId);
}
create = async (): Promise<any> => {
if (!this.draft) return; // skip: empty draft
// skip empty documents if "---" pasted at the beginning or end
const resources = jsYaml.safeLoadAll(this.draft).filter(Boolean);
const createdResources: string[] = [];
const errors: string[] = [];
await Promise.all(
resources.map(data => {
return resourceApplierApi.update(data)
.then(item => createdResources.push(item.metadata.name))
.catch((err: JsonApiErrorParsed) => errors.push(err.toString()));
})
);
if (errors.length) {
errors.forEach(error => Notifications.error(error));
if (!createdResources.length) throw errors[0];
create = async (): Promise<undefined> => {
if (this.error || !this.data.trim()) {
// do not save when field is empty or there is an error
return null;
}
const successMessage = (
<p>
{createdResources.length === 1 ? "Resource" : "Resources"}{" "}
<b>{createdResources.join(", ")}</b> successfully created
</p>
);
Notifications.ok(successMessage);
// skip empty documents
const resources = yaml.loadAll(this.data).filter(Boolean);
const createdResources: string[] = [];
return successMessage;
if (resources.length === 0) {
return void logger.info("Nothing to create");
}
for (const result of await Promise.allSettled(resources.map(resourceApplierApi.update))) {
if (result.status === "fulfilled") {
createdResources.push(result.value.metadata.name);
} else {
Notifications.error(result.reason.toString());
}
}
if (createdResources.length > 0) {
Notifications.ok((
<p>
{createdResources.length === 1 ? "Resource" : "Resources"}{" "}
<b>{createdResources.join(", ")}</b> successfully created
</p>
));
}
return undefined;
};
renderControls() {

View File

@ -42,29 +42,54 @@ export class EditResourceInfoPanel extends React.Component<Props> {
return editResourceStore.getResource(this.tabId);
}
get draft(): string {
return editResourceStore.getData(this.tabId)?.draft;
}
saveDraft(draft: string | object) {
if (typeof draft === "object") {
draft = draft ? jsYaml.safeDump(draft) : undefined;
@computed get draft(): string {
if (!this.isReadyForEditing) {
return ""; // wait until tab's data and kube-object resource are loaded
}
editResourceStore.getData(this.tabId).draft = draft;
const { draft } = editResourceStore.getData(this.tabId);
if (typeof draft === "string") {
return draft;
}
return yaml.dump(this.resource.toPlainObject()); // dump resource first time
}
save = async () => {
const store = editResourceStore.getStore(this.tabId);
const updatedResource: KubeObject = await store.update(this.resource, jsYaml.safeLoad(this.draft));
@action
saveDraft(draft: string | object) {
if (typeof draft === "object") {
draft = draft ? yaml.dump(draft) : undefined;
}
this.saveDraft(updatedResource.toPlainObject()); // update with new resourceVersion to avoid further errors on save
const resourceType = updatedResource.kind;
const resourceName = updatedResource.getName();
editResourceStore.setData(this.tabId, {
firstDraft: draft, // this must be before the next line
...editResourceStore.getData(this.tabId),
draft,
});
}
onChange = (draft: string, error?: string) => {
this.error = error;
this.saveDraft(draft);
};
save = async () => {
if (this.error) {
return null;
}
const store = editResourceStore.getStore(this.tabId);
const currentVersion = yaml.load(this.draft);
const firstVersion = yaml.load(editResourceStore.getData(this.tabId).firstDraft ?? this.draft);
const patches = createPatch(firstVersion, currentVersion);
const updatedResource = await store.patch(this.resource, patches);
editResourceStore.clearInitialDraft(this.tabId);
return (
<p>
{resourceType} <b>{resourceName}</b> updated.
{updatedResource.kind} <b>{updatedResource.getName()}</b> updated.
</p>
);
};

View File

@ -30,7 +30,6 @@ import { navigate } from "../../navigation";
import { cssNames, IClassName } from "../../utils";
import { Icon } from "../icon";
import { HotbarIcon } from "./hotbar-icon";
import { HotbarStore } from "../../../common/hotbar-store";
import { LensKubernetesClusterStatus } from "../../../common/catalog-entities/kubernetes-cluster";
interface Props extends DOMAttributes<HTMLElement> {
@ -88,10 +87,6 @@ export class HotbarEntityIcon extends React.Component<Props> {
return catalogEntityRegistry.activeEntity?.metadata?.uid == item.getId();
}
isPersisted(entity: CatalogEntity) {
return HotbarStore.getInstance().getActive().items.find((item) => item?.entity?.uid === entity.metadata.uid) !== undefined;
}
render() {
if (!this.contextMenu) {
return null;
@ -107,21 +102,13 @@ export class HotbarEntityIcon extends React.Component<Props> {
disabled: !entity
});
const isPersisted = this.isPersisted(entity);
const onOpen = async () => {
const menuItems: CatalogEntityContextMenu[] = [];
if (!isPersisted) {
menuItems.unshift({
title: "Pin to Hotbar",
onClick: () => add(entity, index)
});
} else {
menuItems.unshift({
title: "Unpin from Hotbar",
onClick: () => remove(entity.metadata.uid)
});
}
menuItems.unshift({
title: "Remove from Hotbar",
onClick: () => remove(entity.metadata.uid)
});
this.contextMenu.menuItems = menuItems;

View File

@ -138,7 +138,7 @@ export class HotbarMenu extends React.Component<Props> {
tooltip={`${item.entity.name} (${item.entity.source})`}
menuItems={[
{
title: "Unpin from Hotbar",
title: "Remove from Hotbar",
onClick: () => this.removeItem(item.entity.uid)
}
]}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<path d="M12.6,12.1V2H7C6.5,2,6,2.5,6,3s0.5,1,1,1h1v5c0,1.7-1.3,3-3,3v2h6v7l1,1l1-1v-7h6v-1.9H12.6z"/>
<polygon points="23.6,3.7 21.9,2 19.4,4.5 16.9,2 15.2,3.7 17.7,6.2 15.2,8.7 16.9,10.4 19.4,7.9 21.9,10.4 23.6,8.7 21.1,6.2 "/>
</svg>

After

Width:  |  Height:  |  Size: 487 B

View File

@ -24,7 +24,7 @@ import "./kubeconfig-dialog.scss";
import React from "react";
import { makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
import jsYaml from "js-yaml";
import yaml from "js-yaml";
import type { ServiceAccount } from "../../../common/k8s-api/endpoints";
import { copyToClipboard, cssNames, saveFileDialog } from "../../utils";
import { Button } from "../button";
@ -86,7 +86,7 @@ export class KubeConfigDialog extends React.Component<Props> {
this.close();
});
this.config = config ? jsYaml.dump(config) : "";
this.config = config ? yaml.dump(config) : "";
}
copyToClipboard = () => {

View File

@ -20,7 +20,7 @@
*/
import React from "react";
import { KubernetesCluster } from "../../common/catalog-entities";
import { KubernetesCluster, WebLink } from "../../common/catalog-entities";
import { CatalogEntityDetailRegistry, CatalogEntityDetailsProps } from "../../extensions/registries";
import { DrawerItem, DrawerTitle } from "../components/drawer";
@ -45,6 +45,20 @@ export function initCatalogEntityDetailRegistry() {
</>
),
},
}
},
{
apiVersions: [WebLink.apiVersion],
kind: WebLink.kind,
components: {
Details: ({ entity }: CatalogEntityDetailsProps<WebLink>) => (
<>
<DrawerTitle title="More Information" />
<DrawerItem name="URL">
{entity.spec.url}
</DrawerItem>
</>
),
},
},
]);
}

View File

@ -272,10 +272,10 @@
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.13.17"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.17.tgz#8966d1fc9593bf848602f0662d6b4d0069e3a7ec"
integrity sha512-NCdgJEelPTSh+FEFylhnP1ylq848l1z9t9N0j1Lfbcw0+KXGjsTvUmkxy+voLLXB5SOKMbLLx4jxYliGrYQseA==
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.15.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a"
integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
dependencies:
regenerator-runtime "^0.13.4"
@ -1450,10 +1450,10 @@
dependencies:
"@types/ms" "*"
"@types/dompurify@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.0.2.tgz#94b5c05dc9b8a682a0abb4a8d6f0b82df61baeac"
integrity sha512-WHoQQTziRHm5/Fw/KsUKyh2V+wd3k2QUpJyjUXo8K7d9kMJ5i5wQnGDkO4URkwulhY2HuM/gbX25nSooi6+wUA==
"@types/dompurify@^2.3.1":
version "2.3.1"
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.3.1.tgz#2934adcd31c4e6b02676f9c22f9756e5091c04dd"
integrity sha512-YJth9qa0V/E6/XPH1Jq4BC8uCMmO8V1fKWn8PCvuZcAhMn7q0ez9LW6naQT04UZzjFfAPhyRMZmI2a2rbMlEFA==
dependencies:
"@types/trusted-types" "*"
@ -1611,12 +1611,7 @@
jest-diff "^26.0.0"
pretty-format "^26.0.0"
"@types/js-yaml@^3.12.4":
version "3.12.4"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.4.tgz#7d3b534ec35a0585128e2d332db1403ebe057e25"
integrity sha512-fYMgzN+9e28R81weVN49inn/u798ruU91En1ZnGvSZzCRc5jXx9B2EDhlRaWmcO1RIxFHL8AajRXzxDuJu93+A==
"@types/js-yaml@^4.0.1":
"@types/js-yaml@^4.0.1", "@types/js-yaml@^4.0.2":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.2.tgz#4117a7a378593a218e9d6f0ef44ce6d5d9edf7fa"
integrity sha512-KbeHS/Y4R+k+5sWXEYzAZKuB1yQlZtEghuhRxrVRLaqhtoG5+26JwQsa4HyS3AWX8v1Uwukma5HheduUDskasA==
@ -8217,12 +8212,12 @@ jest-message-util@^26.6.2:
slash "^3.0.0"
stack-utils "^2.0.2"
jest-mock-extended@^1.0.16:
version "1.0.16"
resolved "https://registry.yarnpkg.com/jest-mock-extended/-/jest-mock-extended-1.0.16.tgz#f6a96c795acc2f5ae9ee74d7b048c3b4399c6fc8"
integrity sha512-W1lI7cO0ZYmTQUBZH11nSDKyU36SxAyzBjZNxBWcpFnCKHGNWIPdFA8RJMhmobLQ8WLI4y9AMIbYzMN9q/GiRQ==
jest-mock-extended@^1.0.18:
version "1.0.18"
resolved "https://registry.yarnpkg.com/jest-mock-extended/-/jest-mock-extended-1.0.18.tgz#27a40e882c09f8230243fc6765fe7285b03f2b4b"
integrity sha512-qf1n7lIa2dTxxPIBr+FlXrbj3hnV1sG9DPZsrr2H/8W+Jw0wt6OmeOQsPcjRuW8EXIECC9pDXsSIfEdn+HP7JQ==
dependencies:
ts-essentials "^4.0.0"
ts-essentials "^7.0.2"
jest-mock@^26.6.2:
version "26.6.2"
@ -8447,7 +8442,7 @@ js-sha3@^0.8.0:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^3.13.1, js-yaml@^3.14.0:
js-yaml@^3.13.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
@ -11599,25 +11594,25 @@ react-refresh@^0.9.0:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf"
integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==
react-router-dom@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==
react-router-dom@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363"
integrity sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==
dependencies:
"@babel/runtime" "^7.1.2"
"@babel/runtime" "^7.12.13"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.2.0"
react-router "5.2.1"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.2.0, react-router@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==
react-router@5.2.1, react-router@^5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.1.tgz#4d2e4e9d5ae9425091845b8dbc6d9d276239774d"
integrity sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==
dependencies:
"@babel/runtime" "^7.1.2"
"@babel/runtime" "^7.12.13"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
@ -13671,10 +13666,10 @@ truncate-utf8-bytes@^1.0.0:
dependencies:
utf8-byte-length "^1.0.1"
ts-essentials@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-4.0.0.tgz#506c42b270bbd0465574b90416533175b09205ab"
integrity sha512-uQJX+SRY9mtbKU+g9kl5Fi7AEMofPCvHfJkQlaygpPmHPZrtgaBqbWFOYyiA47RhnSwwnXdepUJrgqUYxoUyhQ==
ts-essentials@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38"
integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==
ts-jest@26.5.6:
version "26.5.6"