mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
refactoring dock-tab stores -- part 1
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
fa90eb833c
commit
f140bc5b70
@ -199,7 +199,6 @@
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-updater": "^4.4.3",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"filehound": "^1.17.4",
|
||||
"fs-extra": "^9.0.1",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
|
||||
@ -22,60 +22,29 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import groupBy from "lodash/groupBy";
|
||||
import filehound from "filehound";
|
||||
import { watch } from "chokidar";
|
||||
import { autoBind } from "../../utils";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { DockTabsStore } from "./dock-tabs.store";
|
||||
import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store";
|
||||
|
||||
export class CreateResourceStore extends DockTabStore<string> {
|
||||
export class CreateResourceStore extends DockTabsStore<string> {
|
||||
constructor() {
|
||||
super({
|
||||
storageKey: "create_resource"
|
||||
});
|
||||
autoBind(this);
|
||||
fs.ensureDirSync(this.userTemplatesFolder);
|
||||
}
|
||||
|
||||
get lensTemplatesFolder():string {
|
||||
return path.resolve(__static, "../templates/create-resource");
|
||||
protected async init() {
|
||||
super.init();
|
||||
await fs.ensureDir(this.userTemplatesFolder);
|
||||
}
|
||||
|
||||
get userTemplatesFolder():string {
|
||||
return path.join(os.homedir(), ".k8slens", "templates");
|
||||
get userTemplatesFolder(): string {
|
||||
return path.resolve(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();
|
||||
});
|
||||
async getBundledTemplate(fileName: string, ext = "yaml") {
|
||||
return import(`../../../../templates/create-resource/${fileName}.${ext}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,13 +20,10 @@
|
||||
*/
|
||||
|
||||
import "./create-resource.scss";
|
||||
|
||||
import React from "react";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import { GroupSelectOption, Select, SelectOption } from "../select";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import jsYaml from "js-yaml";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { computed, makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames } from "../../utils";
|
||||
import { createResourceStore } from "./create-resource.store";
|
||||
@ -46,27 +43,16 @@ interface Props {
|
||||
export class CreateResource extends React.Component<Props> {
|
||||
@observable currentTemplates: Map<string, SelectOption> = new Map();
|
||||
@observable error = "";
|
||||
@observable templates: GroupSelectOption<SelectOption>[] = [];
|
||||
@observable templates = observable.map<string/*filename*/, string>();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v));
|
||||
createResourceStore.watchUserTemplates(() => createResourceStore.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 };
|
||||
// TODO
|
||||
@computed get selectTemplateOptions(): SelectOption<string>[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
@ -77,7 +63,7 @@ export class CreateResource extends React.Component<Props> {
|
||||
return createResourceStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
get currentTemplate() {
|
||||
get selectedTemplate() {
|
||||
return this.currentTemplates.get(this.tabId) ?? null;
|
||||
}
|
||||
|
||||
@ -89,12 +75,14 @@ export class CreateResource extends React.Component<Props> {
|
||||
this.error = error;
|
||||
};
|
||||
|
||||
// FIXME
|
||||
onSelectTemplate = (item: SelectOption) => {
|
||||
this.currentTemplates.set(this.tabId, item);
|
||||
|
||||
fs.readFile(item.value, "utf8").then(value => {
|
||||
createResourceStore.setData(this.tabId, value);
|
||||
});
|
||||
console.log(`SELECTED TEMPLATE: ${this.tabId}`, item);
|
||||
// this.currentTemplates.set(this.tabId, item);
|
||||
//
|
||||
// fs.readFile(item.value, "utf8").then(templateFileContent => {
|
||||
// createResourceStore.setData(this.tabId, templateFileContent);
|
||||
// });
|
||||
};
|
||||
|
||||
create = async () => {
|
||||
@ -137,13 +125,13 @@ export class CreateResource extends React.Component<Props> {
|
||||
<div className="flex gaps align-center">
|
||||
<Select
|
||||
autoConvertOptions={false}
|
||||
className="TemplateSelect"
|
||||
className="SelectResourceTemplate"
|
||||
placeholder="Select Template ..."
|
||||
options={this.templates}
|
||||
options={this.selectTemplateOptions}
|
||||
menuPlacement="top"
|
||||
themeName="outlined"
|
||||
onChange={v => this.onSelectTemplate(v)}
|
||||
value={this.currentTemplate}
|
||||
value={this.selectedTemplate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,108 +0,0 @@
|
||||
/**
|
||||
* 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 { autorun, observable, reaction } from "mobx";
|
||||
import { autoBind, createStorage, StorageHelper, toJS } from "../../utils";
|
||||
import { dockStore, TabId } from "./dock.store";
|
||||
|
||||
export interface DockTabStoreOptions {
|
||||
autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events, default: true
|
||||
storageKey?: string; // save data to persistent storage under the key
|
||||
}
|
||||
|
||||
export type DockTabStorageState<T> = Record<TabId, T>;
|
||||
|
||||
export class DockTabStore<T> {
|
||||
protected storage?: StorageHelper<DockTabStorageState<T>>;
|
||||
protected data = observable.map<TabId, T>();
|
||||
|
||||
constructor(protected options: DockTabStoreOptions = {}) {
|
||||
autoBind(this);
|
||||
|
||||
this.options = {
|
||||
autoInit: true,
|
||||
...this.options,
|
||||
};
|
||||
|
||||
if (this.options.autoInit) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
protected init() {
|
||||
const { storageKey } = this.options;
|
||||
|
||||
// auto-save to local-storage
|
||||
if (storageKey) {
|
||||
this.storage = createStorage(storageKey, {});
|
||||
this.storage.whenReady.then(() => {
|
||||
this.data.replace(this.storage.get());
|
||||
reaction(() => this.toJSON(), data => this.storage.set(data));
|
||||
});
|
||||
}
|
||||
|
||||
// clear data for closed tabs
|
||||
autorun(() => {
|
||||
const currentTabs = dockStore.tabs.map(tab => tab.id);
|
||||
|
||||
Array.from(this.data.keys()).forEach(tabId => {
|
||||
if (!currentTabs.includes(tabId)) {
|
||||
this.clearData(tabId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected finalizeDataForSave(data: T): T {
|
||||
return data;
|
||||
}
|
||||
|
||||
protected toJSON(): DockTabStorageState<T> {
|
||||
const deepCopy = toJS(this.data);
|
||||
|
||||
deepCopy.forEach((tabData, key) => {
|
||||
deepCopy.set(key, this.finalizeDataForSave(tabData));
|
||||
});
|
||||
|
||||
return Object.fromEntries<T>(deepCopy);
|
||||
}
|
||||
|
||||
isReady(tabId: TabId): boolean {
|
||||
return Boolean(this.getData(tabId) !== undefined);
|
||||
}
|
||||
|
||||
getData(tabId: TabId) {
|
||||
return this.data.get(tabId);
|
||||
}
|
||||
|
||||
setData(tabId: TabId, data: T) {
|
||||
this.data.set(tabId, data);
|
||||
}
|
||||
|
||||
clearData(tabId: TabId) {
|
||||
this.data.delete(tabId);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.data.clear();
|
||||
this.storage?.reset();
|
||||
}
|
||||
}
|
||||
141
src/renderer/components/dock/dock-tabs.store.ts
Normal file
141
src/renderer/components/dock/dock-tabs.store.ts
Normal file
@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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 { action, autorun, IReactionDisposer, IReactionOptions, makeObservable, observable, reaction } from "mobx";
|
||||
import { autoBind, createStorage, Disposer, disposer, StorageHelper } from "../../utils";
|
||||
import { dockStore, TabId } from "./dock.store";
|
||||
|
||||
export interface DockTabStoreOptions {
|
||||
autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events, default: true
|
||||
storageKey?: string; // save data to persistent storage under the key
|
||||
}
|
||||
|
||||
export class DockTabsStore<T extends {}> {
|
||||
private storage?: StorageHelper<Record<TabId, T>>; // available only with `options.storageKey`
|
||||
private _data = observable.object<Record<TabId, T>>({});
|
||||
protected watchers = observable.map<TabId, IReactionDisposer | Disposer>([], { deep: false });
|
||||
protected dispose = disposer();
|
||||
|
||||
get data() {
|
||||
if (this.options.storageKey) {
|
||||
return this.storage.get();
|
||||
} else {
|
||||
return this._data;
|
||||
}
|
||||
}
|
||||
|
||||
set data(value: Record<TabId, T>) {
|
||||
if (this.options.storageKey) {
|
||||
this.storage.set(value);
|
||||
} else {
|
||||
this._data = value;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(protected options: DockTabStoreOptions = {}) {
|
||||
autoBind(this);
|
||||
makeObservable(this);
|
||||
|
||||
this.options = {
|
||||
autoInit: true,
|
||||
...this.options,
|
||||
};
|
||||
|
||||
if (this.options.storageKey) {
|
||||
this.storage = createStorage(this.options.storageKey, {}); // starts preloading json file via Node.fs right away
|
||||
}
|
||||
|
||||
if (this.options.autoInit) {
|
||||
this.whenReady.then(() => this.init());
|
||||
}
|
||||
}
|
||||
|
||||
get whenReady(): Promise<any> {
|
||||
return Promise.allSettled([
|
||||
dockStore.whenReady,
|
||||
this.storage?.whenReady,
|
||||
]);
|
||||
}
|
||||
|
||||
protected init() {
|
||||
this.dispose.push(
|
||||
autorun(() => {
|
||||
const docTabIds = dockStore.tabs.map(tab => tab.id);
|
||||
const savedDataTabIds = Object.keys(this.data);
|
||||
|
||||
savedDataTabIds.forEach(tabId => {
|
||||
const isTabClosed = !docTabIds.includes(tabId);
|
||||
|
||||
if (isTabClosed) {
|
||||
this.clearData(tabId); // clear related tab's data
|
||||
this.watchers.get(tabId)?.(); // dispose tab related data watcher
|
||||
dockStore.closeTab(tabId); // make sure dock tab is closed
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
// clean up data watchers (if any)
|
||||
() => this.disposeWatchers(),
|
||||
);
|
||||
}
|
||||
|
||||
protected disposeWatchers() {
|
||||
this.watchers.forEach(dispose => dispose());
|
||||
this.watchers.clear();
|
||||
}
|
||||
|
||||
onDataChange(callback: (entries: [TabId, T][]) => void, opts?: IReactionOptions): IReactionDisposer {
|
||||
return reaction(() => Object.entries(this.data), callback, opts);
|
||||
}
|
||||
|
||||
onTabDataChange(tabId: TabId, callback: (data: T, prevData?: T) => void, opts?: IReactionOptions): IReactionDisposer {
|
||||
return reaction(() => this.data[tabId], callback, opts);
|
||||
}
|
||||
|
||||
isReady(tabId: TabId): boolean {
|
||||
return Boolean(this.getData(tabId) !== undefined);
|
||||
}
|
||||
|
||||
getData(tabId: TabId) {
|
||||
return this.data[tabId];
|
||||
}
|
||||
|
||||
@action
|
||||
setData(tabId: TabId, data: T) {
|
||||
this.data[tabId] = data;
|
||||
}
|
||||
|
||||
@action
|
||||
clearData(tabId: TabId) {
|
||||
delete this.data[tabId];
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.data = {};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.dispose();
|
||||
this.disposeWatchers();
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
@ -88,6 +88,17 @@ export interface DockStorageState {
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
export interface DockTabChangeEvent {
|
||||
selectedTabId?: TabId;
|
||||
tab?: DockTab;
|
||||
prevTab?: DockTab;
|
||||
}
|
||||
|
||||
export interface DockTabChangeEventOptions extends IReactionOptions {
|
||||
kind?: TabKind; // filter: matching by dockStore.selectedTab.kind
|
||||
isVisible?: boolean; // filter: dock and selected tab should be visible
|
||||
}
|
||||
|
||||
export class DockStore implements DockStorageState {
|
||||
constructor() {
|
||||
makeObservable(this);
|
||||
@ -178,8 +189,18 @@ export class DockStore implements DockStorageState {
|
||||
return reaction(() => [this.height, this.fullSize], callback, options);
|
||||
}
|
||||
|
||||
onTabChange(callback: (tabId: TabId) => void, options?: IReactionOptions) {
|
||||
return reaction(() => this.selectedTabId, callback, options);
|
||||
onTabChange(callback?: (evt: DockTabChangeEvent) => void, options: DockTabChangeEventOptions = {}) {
|
||||
const { kind, isVisible, ...reactionOpts } = options;
|
||||
|
||||
return reaction(() => this.selectedTab, ((tab, prevTab) => {
|
||||
if (kind && kind !== tab.kind) return;
|
||||
if (isVisible && !dockStore.isOpen) return;
|
||||
|
||||
callback({
|
||||
tab, prevTab,
|
||||
selectedTabId: this.selectedTabId,
|
||||
});
|
||||
}), reactionOpts);
|
||||
}
|
||||
|
||||
hasTabs() {
|
||||
|
||||
@ -20,8 +20,7 @@
|
||||
*/
|
||||
|
||||
import { autoBind, noop } from "../../utils";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { autorun, IReactionDisposer } from "mobx";
|
||||
import { DockTabsStore } from "./dock-tabs.store";
|
||||
import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store";
|
||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
@ -32,9 +31,7 @@ export interface EditingResource {
|
||||
draft?: string; // edited draft in yaml
|
||||
}
|
||||
|
||||
export class EditResourceStore extends DockTabStore<EditingResource> {
|
||||
private watchers = new Map<TabId, IReactionDisposer>();
|
||||
|
||||
export class EditResourceStore extends DockTabsStore<EditingResource> {
|
||||
constructor() {
|
||||
super({
|
||||
storageKey: "edit_resource_store",
|
||||
@ -44,34 +41,19 @@ export class EditResourceStore extends DockTabStore<EditingResource> {
|
||||
|
||||
protected async init() {
|
||||
super.init();
|
||||
await this.storage.whenReady;
|
||||
|
||||
autorun(() => {
|
||||
Array.from(this.data).forEach(([tabId, { resource }]) => {
|
||||
if (this.watchers.get(tabId)) {
|
||||
return;
|
||||
this.dispose.push(
|
||||
dockStore.onTabChange(({ selectedTabId }) => {
|
||||
const { resource } = this.getData(selectedTabId);
|
||||
const store = apiManager.getStore(resource);
|
||||
|
||||
if (!store?.getByPath(resource)) {
|
||||
store?.loadFromPath(resource).catch(noop); // preload resource by uri
|
||||
}
|
||||
this.watchers.set(tabId, autorun(() => {
|
||||
const store = apiManager.getStore(resource);
|
||||
|
||||
if (store) {
|
||||
const isActiveTab = dockStore.isOpen && dockStore.selectedTabId === tabId;
|
||||
const obj = store.getByPath(resource);
|
||||
|
||||
// preload resource for editing
|
||||
if (!obj && !store.isLoaded && !store.isLoading && isActiveTab) {
|
||||
store.loadFromPath(resource).catch(noop);
|
||||
}
|
||||
// auto-close tab when resource removed from store
|
||||
else if (!obj && store.isLoaded) {
|
||||
dockStore.closeTab(tabId);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
delay: 100 // make sure all kube-object stores are initialized
|
||||
}));
|
||||
});
|
||||
});
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected finalizeDataForSave({ draft, ...data }: EditingResource): EditingResource {
|
||||
@ -97,20 +79,12 @@ export class EditResourceStore extends DockTabStore<EditingResource> {
|
||||
}
|
||||
|
||||
getTabByResource(object: KubeObject): DockTab {
|
||||
const [tabId] = Array.from(this.data).find(([, { resource }]) => {
|
||||
const [tabId] = Object.entries(this.data).find(([, { resource }]) => {
|
||||
return object.selfLink === resource;
|
||||
}) || [];
|
||||
|
||||
return dockStore.getTabById(tabId);
|
||||
}
|
||||
|
||||
reset() {
|
||||
super.reset();
|
||||
Array.from(this.watchers).forEach(([tabId, dispose]) => {
|
||||
this.watchers.delete(tabId);
|
||||
dispose();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const editResourceStore = new EditResourceStore();
|
||||
|
||||
@ -19,9 +19,9 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { action, autorun, makeObservable } from "mobx";
|
||||
import { action, makeObservable, observable } from "mobx";
|
||||
import { dockStore, DockTabCreateSpecific, TabId, TabKind } from "./dock.store";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { DockTabsStore } from "./dock-tabs.store";
|
||||
import { getChartDetails, getChartValues, HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import type { IReleaseUpdateDetails } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { Notifications } from "../notifications";
|
||||
@ -37,23 +37,29 @@ export interface IChartInstallData {
|
||||
lastVersion?: boolean;
|
||||
}
|
||||
|
||||
export class InstallChartStore extends DockTabStore<IChartInstallData> {
|
||||
public versions = new DockTabStore<string[]>();
|
||||
public details = new DockTabStore<IReleaseUpdateDetails>();
|
||||
export class InstallChartStore extends DockTabsStore<IChartInstallData> {
|
||||
details = observable.map<TabId, IReleaseUpdateDetails>();
|
||||
versions = observable.map<TabId, string[]>();
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
storageKey: "install_charts"
|
||||
});
|
||||
makeObservable(this);
|
||||
autorun(() => {
|
||||
const { selectedTab, isOpen } = dockStore;
|
||||
}
|
||||
|
||||
if (selectedTab?.kind === TabKind.INSTALL_CHART && isOpen) {
|
||||
this.loadData(selectedTab.id)
|
||||
.catch(err => Notifications.error(String(err)));
|
||||
}
|
||||
}, { delay: 250 });
|
||||
protected init() {
|
||||
super.init();
|
||||
|
||||
this.dispose.push(
|
||||
dockStore.onTabChange(({ selectedTabId }) => {
|
||||
this.loadData(selectedTabId).catch(err => Notifications.error(String(err)));
|
||||
}, {
|
||||
kind: TabKind.INSTALL_CHART,
|
||||
fireImmediately: true,
|
||||
isVisible: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
@ -64,7 +70,7 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
|
||||
promises.push(this.loadValues(tabId));
|
||||
}
|
||||
|
||||
if (!this.versions.getData(tabId)) {
|
||||
if (!this.versions.get(tabId)) {
|
||||
promises.push(this.loadVersions(tabId));
|
||||
}
|
||||
|
||||
@ -75,11 +81,11 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
|
||||
async loadVersions(tabId: TabId) {
|
||||
const { repo, name, version } = this.getData(tabId);
|
||||
|
||||
this.versions.clearData(tabId); // reset
|
||||
this.versions.delete(tabId); // reset
|
||||
const charts = await getChartDetails(repo, name, { version });
|
||||
const versions = charts.versions.map(chartVersion => chartVersion.version);
|
||||
|
||||
this.versions.setData(tabId, versions);
|
||||
this.versions.set(tabId, versions);
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@ -68,11 +68,11 @@ export class InstallChart extends Component<Props> {
|
||||
}
|
||||
|
||||
get versions() {
|
||||
return installChartStore.versions.getData(this.tabId);
|
||||
return installChartStore.versions.get(this.tabId);
|
||||
}
|
||||
|
||||
get releaseDetails() {
|
||||
return installChartStore.details.getData(this.tabId);
|
||||
return installChartStore.details.get(this.tabId);
|
||||
}
|
||||
|
||||
viewRelease = () => {
|
||||
@ -120,7 +120,7 @@ export class InstallChart extends Component<Props> {
|
||||
repo, namespace, version, values,
|
||||
});
|
||||
|
||||
installChartStore.details.setData(this.tabId, details);
|
||||
installChartStore.details.set(this.tabId, details);
|
||||
|
||||
return (
|
||||
<p>Chart Release <b>{details.release.name}</b> successfully created.</p>
|
||||
|
||||
@ -25,7 +25,7 @@ import { podsStore } from "../+workloads-pods/pods.store";
|
||||
|
||||
import { IPodContainer, Pod } from "../../../common/k8s-api/endpoints";
|
||||
import type { WorkloadKubeObject } from "../../../common/k8s-api/workload-kube-object";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { DockTabsStore } from "./dock-tabs.store";
|
||||
import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store";
|
||||
|
||||
export interface LogTabData {
|
||||
@ -45,15 +45,19 @@ interface WorkloadLogsTabData {
|
||||
workload: WorkloadKubeObject
|
||||
}
|
||||
|
||||
export class LogTabStore extends DockTabStore<LogTabData> {
|
||||
export class LogTabStore extends DockTabsStore<LogTabData> {
|
||||
constructor() {
|
||||
super({
|
||||
storageKey: "pod_logs"
|
||||
});
|
||||
}
|
||||
|
||||
reaction(() => podsStore.items.length, () => {
|
||||
this.updateTabsData();
|
||||
});
|
||||
protected init() {
|
||||
super.init();
|
||||
|
||||
this.dispose.push(
|
||||
reaction(() => podsStore.items.length, () => this.updateTabsData()),
|
||||
);
|
||||
}
|
||||
|
||||
createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): void {
|
||||
@ -108,10 +112,8 @@ export class LogTabStore extends DockTabStore<LogTabData> {
|
||||
});
|
||||
}
|
||||
|
||||
private async updateTabsData() {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (const [tabId, tabData] of this.data) {
|
||||
private updateTabsData() {
|
||||
for (const [tabId, tabData] of Object.entries(this.data)) {
|
||||
const pod = new Pod(tabData.selectedPod);
|
||||
const pods = podsStore.getPodsByOwnerId(pod.getOwnerRefs()[0]?.uid);
|
||||
const isSelectedPodInList = pods.find(item => item.getId() == pod.getId());
|
||||
@ -127,17 +129,8 @@ export class LogTabStore extends DockTabStore<LogTabData> {
|
||||
});
|
||||
|
||||
this.renameTab(tabId);
|
||||
} else {
|
||||
promises.push(this.closeTab(tabId));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
private async closeTab(tabId: string) {
|
||||
this.clearData(tabId);
|
||||
await dockStore.closeTab(tabId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,27 +19,19 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { action, autorun, computed, IReactionDisposer, makeObservable, reaction } from "mobx";
|
||||
import { action, makeObservable, observable, when } from "mobx";
|
||||
import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { DockTabsStore } from "./dock-tabs.store";
|
||||
import { getReleaseValues, HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { releaseStore } from "../+apps-releases/release.store";
|
||||
import { iter } from "../../utils";
|
||||
|
||||
export interface IChartUpgradeData {
|
||||
releaseName: string;
|
||||
releaseNamespace: string;
|
||||
}
|
||||
|
||||
export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
||||
private watchers = new Map<string, IReactionDisposer>();
|
||||
|
||||
values = new DockTabStore<string>();
|
||||
|
||||
@computed
|
||||
private get releaseNameReverseLookup(): Map<string, string> {
|
||||
return new Map(iter.map(this.data, ([id, { releaseName }]) => [releaseName, id]));
|
||||
}
|
||||
export class UpgradeChartStore extends DockTabsStore<IChartUpgradeData> {
|
||||
public values = observable.map<TabId, string>();
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@ -47,82 +39,66 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
||||
});
|
||||
|
||||
makeObservable(this);
|
||||
|
||||
autorun(() => {
|
||||
const { selectedTab, isOpen } = dockStore;
|
||||
|
||||
if (selectedTab?.kind === TabKind.UPGRADE_CHART && isOpen) {
|
||||
this.loadData(selectedTab.id);
|
||||
}
|
||||
}, { delay: 250 });
|
||||
|
||||
autorun(() => {
|
||||
const objects = [...this.data.values()];
|
||||
|
||||
objects.forEach(({ releaseName }) => this.createReleaseWatcher(releaseName));
|
||||
});
|
||||
}
|
||||
|
||||
private createReleaseWatcher(releaseName: string) {
|
||||
if (this.watchers.get(releaseName)) {
|
||||
return;
|
||||
}
|
||||
const dispose = reaction(() => {
|
||||
const release = releaseStore.getByName(releaseName);
|
||||
get whenReady() {
|
||||
return Promise.all([
|
||||
super.whenReady,
|
||||
when(() => releaseStore.isLoaded),
|
||||
]);
|
||||
}
|
||||
|
||||
return release?.getRevision(); // watch changes only by revision
|
||||
},
|
||||
release => {
|
||||
const releaseTab = this.getTabByRelease(releaseName);
|
||||
protected init() {
|
||||
super.init();
|
||||
|
||||
if (!releaseStore.isLoaded || !releaseTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
// auto-reload values if was loaded before
|
||||
if (release) {
|
||||
if (dockStore.selectedTab === releaseTab && this.values.getData(releaseTab.id)) {
|
||||
this.loadValues(releaseTab.id);
|
||||
this.dispose.push(
|
||||
dockStore.onTabChange(({ selectedTabId }) => {
|
||||
if (!this.isLoaded(selectedTabId)) {
|
||||
this.loadData(selectedTabId); // preload just once
|
||||
}
|
||||
}
|
||||
// clean up watcher, close tab if release not exists / was removed
|
||||
else {
|
||||
dispose();
|
||||
this.watchers.delete(releaseName);
|
||||
dockStore.closeTab(releaseTab.id);
|
||||
}
|
||||
});
|
||||
|
||||
this.watchers.set(releaseName, dispose);
|
||||
}, {
|
||||
kind: TabKind.UPGRADE_CHART,
|
||||
isVisible: true,
|
||||
fireImmediately: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
isLoading(tabId = dockStore.selectedTabId) {
|
||||
const values = this.values.getData(tabId);
|
||||
const values = this.values.get(tabId);
|
||||
|
||||
return !releaseStore.isLoaded || values === undefined;
|
||||
}
|
||||
|
||||
isLoaded(tabId: TabId) {
|
||||
return this.values.has(tabId);
|
||||
}
|
||||
|
||||
@action
|
||||
async loadData(tabId: TabId) {
|
||||
const values = this.values.getData(tabId);
|
||||
private async loadData(tabId: TabId) {
|
||||
const values = this.values.get(tabId);
|
||||
|
||||
await Promise.all([
|
||||
!releaseStore.isLoaded && releaseStore.loadFromContextNamespaces(),
|
||||
!values && this.loadValues(tabId)
|
||||
!values && this.reloadValues(tabId)
|
||||
]);
|
||||
}
|
||||
|
||||
@action
|
||||
async loadValues(tabId: TabId) {
|
||||
this.values.clearData(tabId); // reset
|
||||
private async reloadValues(tabId: TabId) {
|
||||
this.values.delete(tabId);
|
||||
const { releaseName, releaseNamespace } = this.getData(tabId);
|
||||
const values = await getReleaseValues(releaseName, releaseNamespace, true);
|
||||
|
||||
this.values.setData(tabId, values);
|
||||
this.values.set(tabId, values);
|
||||
}
|
||||
|
||||
getTabByRelease(releaseName: string): DockTab {
|
||||
return dockStore.getTabById(this.releaseNameReverseLookup.get(releaseName));
|
||||
const entry = Object
|
||||
.entries(this.data)
|
||||
.find(([, data]) => data.releaseName === releaseName);
|
||||
|
||||
return dockStore.getTabById(entry?.[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -73,7 +73,7 @@ export class UpgradeChart extends React.Component<Props> {
|
||||
}
|
||||
|
||||
get value() {
|
||||
return upgradeChartStore.values.getData(this.tabId);
|
||||
return upgradeChartStore.values.get(this.tabId);
|
||||
}
|
||||
|
||||
async loadVersions() {
|
||||
@ -87,7 +87,7 @@ export class UpgradeChart extends React.Component<Props> {
|
||||
}
|
||||
|
||||
onChange = (value: string) => {
|
||||
upgradeChartStore.values.setData(this.tabId, value);
|
||||
upgradeChartStore.values.set(this.tabId, value);
|
||||
};
|
||||
|
||||
onError = (error: string) => {
|
||||
|
||||
@ -21,12 +21,12 @@
|
||||
|
||||
import "./monaco-editor.scss";
|
||||
import React from "react";
|
||||
import { action, computed, makeObservable, observable, reaction, toJS, when } from "mobx";
|
||||
import { action, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { editor, Uri } from "monaco-editor";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { cssNames, disposer } from "../../utils";
|
||||
import { cssNames, disposer, toJS } from "../../utils";
|
||||
|
||||
export interface MonacoEditorProps {
|
||||
id?: string; // associating editor's ID with created model.uri
|
||||
@ -65,6 +65,9 @@ export const defaultEditorProps: Partial<MonacoEditorProps> = {
|
||||
}
|
||||
};
|
||||
|
||||
// FIXME: load resource templates via webpack's dynamic import for create-resource dock tab
|
||||
// FIXME: apply changes of props.options and globalOptions.editor to active editor
|
||||
|
||||
@observer
|
||||
export class MonacoEditor extends React.Component<MonacoEditorProps> {
|
||||
static defaultProps = defaultEditorProps as object;
|
||||
@ -99,6 +102,7 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
const { width, height } = entry.contentRect;
|
||||
|
||||
this.setDimensions(width, height);
|
||||
}
|
||||
});
|
||||
@ -128,13 +132,10 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
|
||||
return this.getModelById(this.editorId);
|
||||
}
|
||||
|
||||
@computed get globalEditorOptions() {
|
||||
return toJS(UserStore.getInstance().editorConfiguration);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
try {
|
||||
this.createEditor();
|
||||
|
||||
if (this.props.autoFocus) {
|
||||
this.editor.focus();
|
||||
}
|
||||
@ -159,6 +160,7 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
|
||||
return;
|
||||
}
|
||||
const { language, theme, readOnly, value: defaultValue, options } = this.props;
|
||||
const globalEditorOptions = toJS(UserStore.getInstance().editorConfiguration);
|
||||
|
||||
this.editor = editor.create(this.containerElem, {
|
||||
model: this.model,
|
||||
@ -166,7 +168,7 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
|
||||
language,
|
||||
theme,
|
||||
readOnly,
|
||||
...this.globalEditorOptions,
|
||||
...globalEditorOptions,
|
||||
...options,
|
||||
});
|
||||
console.info(`[MONACO]: editor created for language=${language}, theme=${theme}`);
|
||||
@ -198,7 +200,7 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
|
||||
() => onContentSizeChangeDisposer.dispose(),
|
||||
this.bindResizeObserver(),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private destroyEditor(): void {
|
||||
if (!this.editor) return;
|
||||
@ -232,12 +234,14 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
|
||||
getModelById(id: string): editor.ITextModel | null {
|
||||
const uri = this.createUri(id);
|
||||
const model = editor.getModels().find(model => String(model.uri) === String(uri));
|
||||
|
||||
if (model) {
|
||||
return model; // model with corresponding props.id exists
|
||||
}
|
||||
|
||||
// creating new temporary model if not exists regarding to props.ID
|
||||
const { language, value } = this.props;
|
||||
|
||||
return editor.createModel(value, language, uri);
|
||||
}
|
||||
|
||||
|
||||
46
yarn.lock
46
yarn.lock
@ -3223,7 +3223,7 @@ bluebird-lst@^1.0.9:
|
||||
dependencies:
|
||||
bluebird "^3.5.5"
|
||||
|
||||
bluebird@^3.4.7, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5:
|
||||
bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||
@ -5981,7 +5981,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
|
||||
assign-symbols "^1.0.0"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
extend@^3.0.0, extend@~3.0.2:
|
||||
extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
@ -6125,15 +6125,6 @@ file-entry-cache@^5.0.1:
|
||||
dependencies:
|
||||
flat-cache "^2.0.1"
|
||||
|
||||
file-js@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/file-js/-/file-js-0.3.0.tgz#fab46bf782346c9294499f1f0d2ad07d838f25d1"
|
||||
integrity sha1-+rRr94I0bJKUSZ8fDSrQfYOPJdE=
|
||||
dependencies:
|
||||
bluebird "^3.4.7"
|
||||
minimatch "^3.0.3"
|
||||
proper-lockfile "^1.2.0"
|
||||
|
||||
file-loader@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d"
|
||||
@ -6147,18 +6138,6 @@ file-uri-to-path@1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
|
||||
|
||||
filehound@^1.17.4:
|
||||
version "1.17.4"
|
||||
resolved "https://registry.yarnpkg.com/filehound/-/filehound-1.17.4.tgz#3f5b76c5b3edc1080311ba802e1ad43179e4291e"
|
||||
integrity sha512-A74hiTADH20bpFbXBNyKtpqN4Guffa+ROmdGJWNnuCRhaD45UVSVoI6McLcpHYmuaOERrzD3gMV3v9VZq/SHeA==
|
||||
dependencies:
|
||||
bluebird "^3.5.1"
|
||||
file-js "0.3.0"
|
||||
lodash "^4.17.10"
|
||||
minimatch "^3.0.4"
|
||||
moment "^2.22.1"
|
||||
unit-compare "^1.0.1"
|
||||
|
||||
filelist@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.1.tgz#f10d1a3ae86c1694808e8f20906f43d4c9132dbb"
|
||||
@ -9605,7 +9584,7 @@ minimalistic-crypto-utils@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
|
||||
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
|
||||
|
||||
minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.3, minimatch@^3.0.4:
|
||||
minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
@ -9731,7 +9710,7 @@ moment-timezone@^0.5.33:
|
||||
dependencies:
|
||||
moment ">= 2.9.0"
|
||||
|
||||
"moment@>= 2.9.0", moment@^2.10.2, moment@^2.14.1, moment@^2.22.1, moment@^2.29.1:
|
||||
"moment@>= 2.9.0", moment@^2.10.2, moment@^2.29.1:
|
||||
version "2.29.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||
@ -11415,16 +11394,6 @@ prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.8.1"
|
||||
|
||||
proper-lockfile@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-1.2.0.tgz#ceff5dd89d3e5f10fb75e1e8e76bc75801a59c34"
|
||||
integrity sha1-zv9d2J0+XxD7deHo52vHWAGlnDQ=
|
||||
dependencies:
|
||||
err-code "^1.0.0"
|
||||
extend "^3.0.0"
|
||||
graceful-fs "^4.1.2"
|
||||
retry "^0.10.0"
|
||||
|
||||
proper-lockfile@^4.1.1, proper-lockfile@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f"
|
||||
@ -14107,13 +14076,6 @@ unique-string@^2.0.0:
|
||||
dependencies:
|
||||
crypto-random-string "^2.0.0"
|
||||
|
||||
unit-compare@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/unit-compare/-/unit-compare-1.0.1.tgz#0c7459f0e5bf53637ea873ca3cee18de2eeca386"
|
||||
integrity sha1-DHRZ8OW/U2N+qHPKPO4Y3i7so4Y=
|
||||
dependencies:
|
||||
moment "^2.14.1"
|
||||
|
||||
universalify@^0.1.0, universalify@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user