mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Refactor templates for create resource tab for ease of testing
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
3e045305ef
commit
0956760626
@ -49,6 +49,7 @@ export * from "./toggle-set";
|
|||||||
export * from "./toJS";
|
export * from "./toJS";
|
||||||
export * from "./type-narrowing";
|
export * from "./type-narrowing";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
export * from "./wait-for-path";
|
||||||
|
|
||||||
import * as iter from "./iter";
|
import * as iter from "./iter";
|
||||||
import * as array from "./array";
|
import * as array from "./array";
|
||||||
|
|||||||
36
src/common/utils/wait-for-path.ts
Normal file
36
src/common/utils/wait-for-path.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { watch } from "chokidar";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for `filePath` and all parent directories to exist.
|
||||||
|
* @param filePath The file path to wait until it exists
|
||||||
|
*/
|
||||||
|
export async function waitForPath(filePath: string): Promise<void> {
|
||||||
|
console.log("waiting for", filePath);
|
||||||
|
const dirOfPath = path.dirname(filePath);
|
||||||
|
|
||||||
|
if (dirOfPath === filePath) {
|
||||||
|
// The root of this filesystem, assume it exists
|
||||||
|
console.log("found", filePath);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
await waitForPath(dirOfPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
watch(dirOfPath, {
|
||||||
|
depth: 0,
|
||||||
|
}).on("all", (event, path) => {
|
||||||
|
if ((event === "add" || event === "addDir") && path === filePath) {
|
||||||
|
console.log("found", filePath);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import type { GroupSelectOption, SelectOption } from "../../select";
|
||||||
|
import lensCreateResourceTemplatesInjectable from "./lens-templates.injectable";
|
||||||
|
import userCreateResourceTemplatesInjectable from "./user-templates.injectable";
|
||||||
|
|
||||||
|
export type RawTemplates = [group: string, items: [file: string, contents: string][]];
|
||||||
|
|
||||||
|
const createResourceTemplatesInjectable = getInjectable({
|
||||||
|
instantiate: (di) => {
|
||||||
|
const lensResourceTemplates = di.inject(lensCreateResourceTemplatesInjectable);
|
||||||
|
const userResourceTemplates = di.inject(userCreateResourceTemplatesInjectable);
|
||||||
|
|
||||||
|
return computed(() => {
|
||||||
|
const res = [
|
||||||
|
...userResourceTemplates.get(),
|
||||||
|
lensResourceTemplates,
|
||||||
|
];
|
||||||
|
|
||||||
|
return res.map(([group, items]) => ({
|
||||||
|
label: group,
|
||||||
|
options: items.map(([label, value]) => ({ label, value })),
|
||||||
|
}) as GroupSelectOption<SelectOption<string>>);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createResourceTemplatesInjectable;
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const extensionMatchers = [
|
||||||
|
/\.yaml$/,
|
||||||
|
/\.yml$/,
|
||||||
|
/\.json$/,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a fileName matches a yaml or json file name structure
|
||||||
|
* @param fileName The fileName to check
|
||||||
|
*/
|
||||||
|
export function hasCorrectExtension(fileName: string): boolean {
|
||||||
|
return extensionMatchers.some(matcher => matcher.test(fileName));
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import path from "path";
|
||||||
|
import { readdir, readFile } from "fs/promises";
|
||||||
|
import { hasCorrectExtension } from "./has-correct-extension";
|
||||||
|
import type { RawTemplates } from "./create-resource-templates.injectable";
|
||||||
|
|
||||||
|
const templatesFolder = path.resolve(__static, "../templates/create-resource");
|
||||||
|
|
||||||
|
async function getTemplates() {
|
||||||
|
/**
|
||||||
|
* Mapping between file names and their contents
|
||||||
|
*/
|
||||||
|
const templates: [file: string, contents: string][] = [];
|
||||||
|
|
||||||
|
for (const dirEntry of await readdir(templatesFolder)) {
|
||||||
|
if (hasCorrectExtension(dirEntry)) {
|
||||||
|
templates.push([path.parse(dirEntry).name, await readFile(path.join(templatesFolder, dirEntry), "utf-8")]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lensTemplatePaths: RawTemplates;
|
||||||
|
|
||||||
|
const lensCreateResourceTemplatesInjectable = getInjectable({
|
||||||
|
setup: async () => {
|
||||||
|
lensTemplatePaths = ["lens", await getTemplates()];
|
||||||
|
},
|
||||||
|
instantiate: () => lensTemplatePaths,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default lensCreateResourceTemplatesInjectable;
|
||||||
@ -3,13 +3,7 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "fs-extra";
|
import type { StorageHelper } from "../../../utils";
|
||||||
import path from "path";
|
|
||||||
import os from "os";
|
|
||||||
import groupBy from "lodash/groupBy";
|
|
||||||
import filehound from "filehound";
|
|
||||||
import { watch } from "chokidar";
|
|
||||||
import { autoBind, StorageHelper } from "../../../utils";
|
|
||||||
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
|
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
@ -21,48 +15,5 @@ export class CreateResourceTabStore extends DockTabStore<string> {
|
|||||||
super(dependencies, {
|
super(dependencies, {
|
||||||
storageKey: "create_resource",
|
storageKey: "create_resource",
|
||||||
});
|
});
|
||||||
|
|
||||||
autoBind(this);
|
|
||||||
fs.ensureDirSync(this.userTemplatesFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
get lensTemplatesFolder():string {
|
|
||||||
return path.resolve(__static, "../templates/create-resource");
|
|
||||||
}
|
|
||||||
|
|
||||||
get userTemplatesFolder():string {
|
|
||||||
return path.join(os.homedir(), ".k8slens", "templates");
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTemplates(templatesPath: string, defaultGroup: string) {
|
|
||||||
const templates = await filehound.create().path(templatesPath).ext(["yaml", "json"]).depth(1).find();
|
|
||||||
|
|
||||||
return templates ? this.groupTemplates(templates, templatesPath, defaultGroup) : {};
|
|
||||||
}
|
|
||||||
|
|
||||||
groupTemplates(templates: string[], templatesPath: string, defaultGroup: string) {
|
|
||||||
return groupBy(templates, (v:string) =>
|
|
||||||
path.relative(templatesPath, v).split(path.sep).length>1
|
|
||||||
? path.parse(path.relative(templatesPath, v)).dir
|
|
||||||
: defaultGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMergedTemplates() {
|
|
||||||
const userTemplates = await this.getTemplates(this.userTemplatesFolder, "ungrouped");
|
|
||||||
const lensTemplates = await this.getTemplates(this.lensTemplatesFolder, "lens");
|
|
||||||
|
|
||||||
return { ...userTemplates, ...lensTemplates };
|
|
||||||
}
|
|
||||||
|
|
||||||
async watchUserTemplates(callback: ()=> void){
|
|
||||||
watch(this.userTemplatesFolder, {
|
|
||||||
depth: 1,
|
|
||||||
ignoreInitial: true,
|
|
||||||
awaitWriteFinish: {
|
|
||||||
stabilityThreshold: 500,
|
|
||||||
},
|
|
||||||
}).on("all", () => {
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { computed, IComputedValue, observable } from "mobx";
|
||||||
|
import path from "path";
|
||||||
|
import os from "os";
|
||||||
|
import { getOrInsert, waitForPath } from "../../../utils";
|
||||||
|
import { watch } from "chokidar";
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
import logger from "../../../../common/logger";
|
||||||
|
import { hasCorrectExtension } from "./has-correct-extension";
|
||||||
|
import type { RawTemplates } from "./create-resource-templates.injectable";
|
||||||
|
|
||||||
|
const userTemplatesFolder = path.join(os.homedir(), ".k8slens", "templates");
|
||||||
|
|
||||||
|
function groupTemplates(templates: Map<string, string>): RawTemplates[] {
|
||||||
|
const res = new Map<string, [string, string][]>();
|
||||||
|
|
||||||
|
for (const [filePath, contents] of templates) {
|
||||||
|
const rawRelative = path.dirname(path.relative(userTemplatesFolder, filePath));
|
||||||
|
const title = rawRelative === "."
|
||||||
|
? "ungrouped"
|
||||||
|
: rawRelative;
|
||||||
|
|
||||||
|
getOrInsert(res, title, []).push([path.parse(filePath).name, contents]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...res.entries()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchUserCreateResourceTemplates(): IComputedValue<RawTemplates[]> {
|
||||||
|
/**
|
||||||
|
* Map between filePaths and template contents
|
||||||
|
*/
|
||||||
|
const templates = observable.map<string, string>();
|
||||||
|
|
||||||
|
const onAddOrChange = async (filePath: string) => {
|
||||||
|
if (!hasCorrectExtension(filePath)) {
|
||||||
|
// ignore non yaml or json files
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contents = await readFile(filePath, "utf-8");
|
||||||
|
|
||||||
|
templates.set(filePath, contents);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code === "ENOENT") {
|
||||||
|
// ignore, file disappeared
|
||||||
|
} else {
|
||||||
|
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while reading ${filePath}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onUnlink = (filePath: string) => {
|
||||||
|
templates.delete(filePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
waitForPath(userTemplatesFolder)
|
||||||
|
.then(() => {
|
||||||
|
console.log("watching", userTemplatesFolder);
|
||||||
|
watch(userTemplatesFolder, {
|
||||||
|
disableGlobbing: true,
|
||||||
|
ignorePermissionErrors: true,
|
||||||
|
usePolling: false,
|
||||||
|
awaitWriteFinish: {
|
||||||
|
pollInterval: 100,
|
||||||
|
stabilityThreshold: 1000,
|
||||||
|
},
|
||||||
|
ignoreInitial: false,
|
||||||
|
atomic: 150, // for "atomic writes"
|
||||||
|
})
|
||||||
|
.on("add", onAddOrChange)
|
||||||
|
.on("change", onAddOrChange)
|
||||||
|
.on("unlink", onUnlink);
|
||||||
|
});
|
||||||
|
|
||||||
|
return computed(() => groupTemplates(templates));
|
||||||
|
}
|
||||||
|
|
||||||
|
const userCreateResourceTemplatesInjectable = getInjectable({
|
||||||
|
instantiate: () => watchUserCreateResourceTemplates(),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default userCreateResourceTemplatesInjectable;
|
||||||
@ -4,11 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import path from "path";
|
|
||||||
import fs from "fs-extra";
|
|
||||||
import { GroupSelectOption, Select, SelectOption } from "../../select";
|
import { GroupSelectOption, Select, SelectOption } from "../../select";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { makeObservable, observable } from "mobx";
|
import { IComputedValue, makeObservable, observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import type { CreateResourceTabStore } from "./store";
|
import type { CreateResourceTabStore } from "./store";
|
||||||
import type { DockTab } from "../dock/store";
|
import type { DockTab } from "../dock/store";
|
||||||
@ -23,44 +21,27 @@ import { apiManager } from "../../../../common/k8s-api/api-manager";
|
|||||||
import { prevDefault } from "../../../utils";
|
import { prevDefault } from "../../../utils";
|
||||||
import { navigate } from "../../../navigation";
|
import { navigate } from "../../../navigation";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import createResourceTabStoreInjectable
|
import createResourceTabStoreInjectable from "./store.injectable";
|
||||||
from "./store.injectable";
|
import createResourceTemplatesInjectable from "./create-resource-templates.injectable";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tab: DockTab;
|
tab: DockTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
|
createResourceTemplates: IComputedValue<GroupSelectOption<SelectOption>[]>;
|
||||||
createResourceTabStore: CreateResourceTabStore;
|
createResourceTabStore: CreateResourceTabStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class NonInjectedCreateResource extends React.Component<Props & Dependencies> {
|
class NonInjectedCreateResource extends React.Component<Props & Dependencies> {
|
||||||
@observable currentTemplates: Map<string, SelectOption> = new Map();
|
|
||||||
@observable error = "";
|
@observable error = "";
|
||||||
@observable templates: GroupSelectOption<SelectOption>[] = [];
|
|
||||||
|
|
||||||
constructor(props: Props & Dependencies) {
|
constructor(props: Props & Dependencies) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.createResourceTabStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v));
|
|
||||||
this.props.createResourceTabStore.watchUserTemplates(() => this.props.createResourceTabStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v)));
|
|
||||||
}
|
|
||||||
|
|
||||||
updateGroupSelectOptions(templates: Record<string, string[]>) {
|
|
||||||
this.templates = Object.entries(templates)
|
|
||||||
.map(([name, grouping]) => this.convertToGroup(name, grouping));
|
|
||||||
}
|
|
||||||
|
|
||||||
convertToGroup(group: string, items: string[]): GroupSelectOption {
|
|
||||||
const options = items.map(v => ({ label: path.parse(v).name, value: v }));
|
|
||||||
|
|
||||||
return { label: group, options };
|
|
||||||
}
|
|
||||||
|
|
||||||
get tabId() {
|
get tabId() {
|
||||||
return this.props.tab.id;
|
return this.props.tab.id;
|
||||||
}
|
}
|
||||||
@ -69,10 +50,6 @@ class NonInjectedCreateResource extends React.Component<Props & Dependencies> {
|
|||||||
return this.props.createResourceTabStore.getData(this.tabId);
|
return this.props.createResourceTabStore.getData(this.tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
get currentTemplate() {
|
|
||||||
return this.currentTemplates.get(this.tabId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange = (value: string) => {
|
onChange = (value: string) => {
|
||||||
this.error = ""; // reset first, validation goes later
|
this.error = ""; // reset first, validation goes later
|
||||||
this.props.createResourceTabStore.setData(this.tabId, value);
|
this.props.createResourceTabStore.setData(this.tabId, value);
|
||||||
@ -82,17 +59,14 @@ class NonInjectedCreateResource extends React.Component<Props & Dependencies> {
|
|||||||
this.error = error.toString();
|
this.error = error.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
onSelectTemplate = (item: SelectOption) => {
|
onSelectTemplate = (item: SelectOption<string>) => {
|
||||||
this.currentTemplates.set(this.tabId, item);
|
this.props.createResourceTabStore.setData(this.tabId, item.value);
|
||||||
fs.readFile(item.value, "utf8").then(v => {
|
|
||||||
this.props.createResourceTabStore.setData(this.tabId, v);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
create = async (): Promise<undefined> => {
|
create = async (): Promise<void> => {
|
||||||
if (this.error || !this.data.trim()) {
|
if (this.error || !this.data.trim()) {
|
||||||
// do not save when field is empty or there is an error
|
// do not save when field is empty or there is an error
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip empty documents
|
// skip empty documents
|
||||||
@ -104,11 +78,12 @@ class NonInjectedCreateResource extends React.Component<Props & Dependencies> {
|
|||||||
|
|
||||||
const creatingResources = resources.map(async (resource: string) => {
|
const creatingResources = resources.map(async (resource: string) => {
|
||||||
try {
|
try {
|
||||||
const data: KubeJsonApiData = await resourceApplierApi.update(resource);
|
const data = await resourceApplierApi.update(resource) as KubeJsonApiData;
|
||||||
const { kind, apiVersion, metadata: { name, namespace }} = data;
|
const { kind, apiVersion, metadata: { name, namespace }} = data;
|
||||||
const resourceLink = apiManager.lookupApiLink({ kind, apiVersion, name, namespace });
|
|
||||||
|
|
||||||
const showDetails = () => {
|
const showDetails = () => {
|
||||||
|
const resourceLink = apiManager.lookupApiLink({ kind, apiVersion, name, namespace });
|
||||||
|
|
||||||
navigate(getDetailsUrl(resourceLink));
|
navigate(getDetailsUrl(resourceLink));
|
||||||
close();
|
close();
|
||||||
};
|
};
|
||||||
@ -124,8 +99,6 @@ class NonInjectedCreateResource extends React.Component<Props & Dependencies> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await Promise.allSettled(creatingResources);
|
await Promise.allSettled(creatingResources);
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
renderControls() {
|
renderControls() {
|
||||||
@ -136,11 +109,10 @@ class NonInjectedCreateResource extends React.Component<Props & Dependencies> {
|
|||||||
controlShouldRenderValue={false} // always keep initial placeholder
|
controlShouldRenderValue={false} // always keep initial placeholder
|
||||||
className="TemplateSelect"
|
className="TemplateSelect"
|
||||||
placeholder="Select Template ..."
|
placeholder="Select Template ..."
|
||||||
options={this.templates}
|
options={this.props.createResourceTemplates.get()}
|
||||||
menuPlacement="top"
|
menuPlacement="top"
|
||||||
themeName="outlined"
|
themeName="outlined"
|
||||||
onChange={v => this.onSelectTemplate(v)}
|
onChange={ this.onSelectTemplate}
|
||||||
value={this.currentTemplate}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -170,13 +142,10 @@ class NonInjectedCreateResource extends React.Component<Props & Dependencies> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateResource = withInjectables<Dependencies, Props>(
|
export const CreateResource = withInjectables<Dependencies, Props>(NonInjectedCreateResource, {
|
||||||
NonInjectedCreateResource,
|
getProps: (di, props) => ({
|
||||||
|
createResourceTabStore: di.inject(createResourceTabStoreInjectable),
|
||||||
{
|
createResourceTemplates: di.inject(createResourceTemplatesInjectable),
|
||||||
getProps: (di, props) => ({
|
...props,
|
||||||
createResourceTabStore: di.inject(createResourceTabStoreInjectable),
|
}),
|
||||||
...props,
|
});
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import dockStoreInjectable from "./dock/store.injectable";
|
|||||||
|
|
||||||
interface Props extends OptionalProps {
|
interface Props extends OptionalProps {
|
||||||
tabId: TabId;
|
tabId: TabId;
|
||||||
submit?: () => Promise<ReactNode | string>;
|
submit?: () => Promise<ReactNode | string | void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OptionalProps {
|
interface OptionalProps {
|
||||||
@ -80,7 +80,7 @@ class NonInjectedInfoPanel extends Component<Props & Dependencies> {
|
|||||||
try {
|
try {
|
||||||
const result = await this.props.submit();
|
const result = await this.props.submit();
|
||||||
|
|
||||||
if (showNotifications) Notifications.ok(result);
|
if (showNotifications && result) Notifications.ok(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (showNotifications) Notifications.error(error.toString());
|
if (showNotifications) Notifications.error(error.toString());
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user