UNPKG

@finos/legend-application-studio

Version:
626 lines 30.2 kB
/** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { computed, action, observable, makeObservable, flow } from 'mobx'; import { guaranteeType, uuid, isNonNullable, UnsupportedOperationError, uniq, addUniqueEntry, assertErrorThrown, filterByType, removeSuffix, } from '@finos/legend-shared'; import { ElementEditorState } from './ElementEditorState.js'; import { ConnectionEditorState } from './connection/ConnectionEditorState.js'; import { getMappingElementSource } from './mapping/MappingEditorState.js'; import { getAllClassMappings, PackageableRuntime, Runtime, EngineRuntime, IdentifiedConnection, RuntimePointer, ConnectionPointer, Store, ModelStore, PureModelConnection, JsonModelConnection, XmlModelConnection, Class, RootFlatDataRecordType, FlatData, FlatDataConnection, PackageableElementExplicitReference, Database, TableAlias, DatabaseType, RelationalDatabaseConnection, StaticDatasourceSpecification, DefaultH2AuthenticationStrategy, ModelChainConnection, isStubbed_StoreConnections, getAllIdentifiedConnections, generateIdentifiedConnectionId, LakehouseRuntime, ConcreteFunctionDefinition, } from '@finos/legend-graph'; import { packageableElementReference_setValue } from '../../../graph-modifier/DomainGraphModifierHelper.js'; import { runtime_addIdentifiedConnection, runtime_addMapping, runtime_addUniqueStoreConnectionsForStore, runtime_deleteIdentifiedConnection, runtime_deleteMapping, } from '../../../graph-modifier/DSL_Mapping_GraphModifierHelper.js'; import { CUSTOM_LABEL } from '../../NewElementState.js'; import { lakehouseRuntime_setConnection } from '../../../graph-modifier/DSL_LakehouseRuntime_GraphModifierHelper.js'; export const getClassMappingStore = (setImplementation, editorStore) => { const sourceElement = getMappingElementSource(setImplementation, editorStore.pluginManager.getApplicationPlugins()); if (sourceElement instanceof Class) { return ModelStore.INSTANCE; } else if (sourceElement instanceof RootFlatDataRecordType) { return sourceElement._OWNER._OWNER; } else if (sourceElement instanceof TableAlias) { return sourceElement.relation.ownerReference.value; } else if (sourceElement instanceof ConcreteFunctionDefinition) { return undefined; } if (sourceElement) { const extraInstanceSetImplementationStoreExtractors = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraInstanceSetImplementationStoreExtractors?.() ?? []); for (const extractor of extraInstanceSetImplementationStoreExtractors) { const instanceSetImplementationStore = extractor(sourceElement); if (instanceSetImplementationStore) { return instanceSetImplementationStore; } } throw new UnsupportedOperationError(`Can't extract store for class mapping: no compatible extractor available from plugins`, setImplementation); } return undefined; }; const getStoresFromMappings = (mappings, editorStore) => uniq(mappings.flatMap((mapping) => getAllClassMappings(mapping) .map((setImplementation) => getClassMappingStore(setImplementation, editorStore)) .filter(isNonNullable))); /** * Since model connection are pretty tedious to add, we automatically create new connections for mapped classes * as we add/change mapping for the runtime * * NOTE: as of now, to be safe and simple, we will not remove the connections as we remove the mapping from the runtime */ export const decorateRuntimeWithNewMapping = (runtime, mapping, editorStore) => { const runtimeValue = runtime instanceof RuntimePointer ? runtime.packageableRuntime.value.runtimeValue : guaranteeType(runtime, EngineRuntime); getStoresFromMappings([mapping], editorStore).forEach((store) => runtime_addUniqueStoreConnectionsForStore(runtimeValue, store, editorStore.changeDetectionState.observerContext)); const sourceClasses = []; mapping.classMappings.forEach((classMapping) => { const mappingSource = getMappingElementSource(classMapping, editorStore.pluginManager.getApplicationPlugins()); if (mappingSource instanceof Class) { addUniqueEntry(sourceClasses, mappingSource); } }); let classesSpecifiedInModelConnections = []; runtimeValue.connections.forEach((storeConnections) => { if (storeConnections.store.value instanceof ModelStore) { classesSpecifiedInModelConnections = storeConnections.storeConnections .filter((identifiedConnection) => identifiedConnection.connection instanceof JsonModelConnection || identifiedConnection.connection instanceof XmlModelConnection) .map((identifiedConnection) => identifiedConnection.connection.class.value); } }); sourceClasses .filter((_class) => !classesSpecifiedInModelConnections.includes(_class)) .forEach((_class) => runtime_addIdentifiedConnection(runtimeValue, new IdentifiedConnection(generateIdentifiedConnectionId(runtimeValue), new JsonModelConnection(PackageableElementExplicitReference.create(ModelStore.INSTANCE), PackageableElementExplicitReference.create(_class))), editorStore.changeDetectionState.observerContext)); }; export const isConnectionForStore = (connection, store) => { const connectionValue = connection instanceof ConnectionPointer ? connection.packageableConnection.value.connectionValue : connection; if (connectionValue instanceof PureModelConnection) { return store instanceof ModelStore; } return connectionValue.store?.value === store; }; export const isConnectionForModelStoreWithClass = (connection, _class) => { const connectionValue = connection instanceof ConnectionPointer ? connection.packageableConnection.value.connectionValue : connection; if (connectionValue instanceof JsonModelConnection || connectionValue instanceof XmlModelConnection) { return connectionValue.class.value === _class; } else if (connectionValue instanceof ModelChainConnection) { return connectionValue.mappings.some((mapping) => mapping.value.classMappings.some((classMapping) => classMapping.class.value === _class)); } return false; }; export const getConnectionsForModelStoreWithClass = (connections, _class) => uniq(connections.filter((connection) => isConnectionForModelStoreWithClass(connection, _class))); /** * Derive the stores from the runtime's mappings and then partition the list of runtime's connections based on the store */ export const getRuntimeExplorerTreeData = (runtime, editorStore) => { const runtimeValue = runtime instanceof RuntimePointer ? runtime.packageableRuntime.value.runtimeValue : guaranteeType(runtime, EngineRuntime); const rootIds = []; const nodes = new Map(); const allSourceClassesFromMappings = uniq(runtimeValue.mappings.flatMap((mapping) => getAllClassMappings(mapping.value) .map((setImplementation) => getMappingElementSource(setImplementation, editorStore.pluginManager.getApplicationPlugins())) .filter(filterByType(Class)))); // runtime (root) const runtimeNode = { data: runtimeValue, id: 'runtime', label: runtime instanceof RuntimePointer ? runtime.packageableRuntime.value.name : CUSTOM_LABEL, isOpen: true, childrenIds: [], }; nodes.set(runtimeNode.id, runtimeNode); addUniqueEntry(rootIds, runtimeNode.id); // stores (1st level) runtimeValue.connections .map((storeConnections) => storeConnections.store.value) .sort((a, b) => a.name.localeCompare(b.name)) .forEach((store) => { const childrenIds = []; if (store instanceof ModelStore) { // expand ModelStore to show source classes const classes = uniq(getAllIdentifiedConnections(runtimeValue) .filter((identifiedConnection) => identifiedConnection.connection.store?.value instanceof ModelStore) .map((identifiedConnection) => { const connectionValue = identifiedConnection.connection instanceof ConnectionPointer ? identifiedConnection.connection.packageableConnection.value .connectionValue : identifiedConnection.connection; if (connectionValue instanceof JsonModelConnection || connectionValue instanceof XmlModelConnection) { return connectionValue.class.value; } return undefined; }) .concat(allSourceClassesFromMappings)); // make sure we add classes (from mappings) that we expect to have connections for // classes (2nd level) - only for `ModelStore` classes.filter(isNonNullable).forEach((_class) => { const classNode = { data: _class, id: _class.path, label: _class.name, }; nodes.set(classNode.id, classNode); addUniqueEntry(childrenIds, classNode.id); }); } const storeNode = { data: store, id: store.path, label: store.name, isOpen: true, childrenIds, }; addUniqueEntry(runtimeNode.childrenIds, storeNode.id); nodes.set(storeNode.id, storeNode); }); return { rootIds, nodes }; }; export class RuntimeEditorTabState { uuid = uuid(); editorStore; runtimeEditorState; constructor(editorStore, runtimeEditorState) { this.editorStore = editorStore; this.runtimeEditorState = runtimeEditorState; } } export class IdentifiedConnectionEditorState { editorStore; idenfitiedConnection; connectionEditorState; constructor(editorStore, idenfitiedConnection) { this.editorStore = editorStore; this.idenfitiedConnection = idenfitiedConnection; this.connectionEditorState = new ConnectionEditorState(this.editorStore, idenfitiedConnection.connection); } } export class IdentifiedConnectionsEditorTabState extends RuntimeEditorTabState { identifiedConnectionEditorState; constructor(editorStore, runtimeEditorState) { super(editorStore, runtimeEditorState); makeObservable(this, { identifiedConnectionEditorState: observable, openIdentifiedConnection: action, addIdentifiedConnection: action, }); } openIdentifiedConnection(identifiedConnection) { this.identifiedConnectionEditorState = new IdentifiedConnectionEditorState(this.editorStore, identifiedConnection); } addIdentifiedConnection(packageableConnection) { let newConnection; if (packageableConnection && this.packageableConnections.includes(packageableConnection)) { newConnection = new ConnectionPointer(PackageableElementExplicitReference.create(packageableConnection)); } else if (this.packageableConnections.length) { newConnection = new ConnectionPointer(PackageableElementExplicitReference.create(this.packageableConnections[0])); } else { try { newConnection = this.createDefaultConnection(); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.notificationService.notifyWarning(error.message); return; } } const newIdentifiedConnection = new IdentifiedConnection(generateIdentifiedConnectionId(this.runtimeEditorState.runtimeValue), newConnection); runtime_addIdentifiedConnection(this.runtimeEditorState.runtimeValue, newIdentifiedConnection, this.editorStore.changeDetectionState.observerContext); this.openIdentifiedConnection(newIdentifiedConnection); } getConnectionEditorState() { return this.identifiedConnectionEditorState?.connectionEditorState .connection instanceof ConnectionPointer ? new ConnectionEditorState(this.editorStore, this.identifiedConnectionEditorState.connectionEditorState.connection.packageableConnection.value.connectionValue) : this.identifiedConnectionEditorState?.connectionEditorState; } } export class IdentifiedConnectionsPerStoreEditorTabState extends IdentifiedConnectionsEditorTabState { store; constructor(editorStore, runtimeEditorState, store) { super(editorStore, runtimeEditorState); makeObservable(this, { store: observable, identifiedConnections: computed, packageableConnections: computed, deleteIdentifiedConnection: action, }); this.store = store; if (this.identifiedConnections.length) { this.openIdentifiedConnection(this.identifiedConnections[0]); } } get identifiedConnections() { return (this.runtimeEditorState.runtimeValue.connections.find((storeConnections) => storeConnections.store.value === this.store)?.storeConnections ?? []); } get packageableConnections() { return this.editorStore.graphManagerState.graph.ownConnections.filter((connection) => isConnectionForStore(connection.connectionValue, this.store)); } createDefaultConnection() { if (this.store instanceof FlatData) { return new FlatDataConnection(PackageableElementExplicitReference.create(this.store)); } else if (this.store instanceof Database) { return new RelationalDatabaseConnection(PackageableElementExplicitReference.create(this.store), DatabaseType.H2, new StaticDatasourceSpecification('host', 80, 'db'), new DefaultH2AuthenticationStrategy()); } const extraDefaultConnectionValueBuilders = this.editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraDefaultConnectionValueBuilders?.() ?? []); for (const builder of extraDefaultConnectionValueBuilders) { const defaultConnection = builder(this.store); if (defaultConnection) { return defaultConnection; } } throw new UnsupportedOperationError(`Can't build default connection for the specified store: no compatible builder available from plugins`, this.store); } deleteIdentifiedConnection(identifiedConnection) { runtime_deleteIdentifiedConnection(this.runtimeEditorState.runtimeValue, identifiedConnection); if (identifiedConnection.connection === this.identifiedConnectionEditorState?.connectionEditorState.connection) { this.identifiedConnectionEditorState = undefined; } this.runtimeEditorState.reprocessRuntimeExplorerTree(); if (!this.identifiedConnections.length) { const stores = getStoresFromMappings(this.runtimeEditorState.runtimeValue.mappings.map((mapping) => mapping.value), this.editorStore); if (!stores.includes(this.store)) { this.runtimeEditorState.openTabFor(this.runtimeEditorState.runtimeValue); } } } } export class IdentifiedConnectionsPerClassEditorTabState extends IdentifiedConnectionsEditorTabState { class; constructor(editorStore, runtimeEditorState, _class) { super(editorStore, runtimeEditorState); makeObservable(this, { class: observable, identifiedConnections: computed, packageableConnections: computed, deleteIdentifiedConnection: action, }); this.class = _class; if (this.identifiedConnections.length) { this.openIdentifiedConnection(this.identifiedConnections[0]); } } get identifiedConnections() { return getAllIdentifiedConnections(this.runtimeEditorState.runtimeValue).filter((identifiedConnection) => isConnectionForModelStoreWithClass(identifiedConnection.connection, this.class)); } get packageableConnections() { return this.editorStore.graphManagerState.graph.ownConnections.filter((connection) => isConnectionForModelStoreWithClass(connection.connectionValue, this.class)); } createDefaultConnection() { return new JsonModelConnection(PackageableElementExplicitReference.create(ModelStore.INSTANCE), PackageableElementExplicitReference.create(this.class)); } deleteIdentifiedConnection(identifiedConnection) { runtime_deleteIdentifiedConnection(this.runtimeEditorState.runtimeValue, identifiedConnection); if (identifiedConnection.connection === this.identifiedConnectionEditorState?.connectionEditorState.connection) { this.identifiedConnectionEditorState = undefined; } this.runtimeEditorState.reprocessRuntimeExplorerTree(); if (!this.identifiedConnections.length) { const allSourceClassesFromMappings = uniq(this.runtimeEditorState.runtimeValue.mappings.flatMap((mapping) => getAllClassMappings(mapping.value) .map((setImplementation) => getMappingElementSource(setImplementation, this.editorStore.pluginManager.getApplicationPlugins())) .filter(filterByType(Class)))); if (!allSourceClassesFromMappings.includes(this.class)) { this.runtimeEditorState.openTabFor(this.runtimeEditorState.runtimeValue); } } } } export class RuntimeEditorRuntimeTabState extends RuntimeEditorTabState { } export class EngineRuntimeEditorState { editorStore; state; runtimeValue; explorerTreeData; currentTabState; constructor(state, value) { this.editorStore = state.editorStore; this.state = state; this.runtimeValue = value; makeObservable(this, { explorerTreeData: observable.ref, currentTabState: observable, setExplorerTreeData: action, addMapping: action, deleteMapping: action, changeMapping: action, addIdentifiedConnection: action, openTabFor: action, decorateRuntimeConnections: action, cleanUpDecoration: action, reprocessRuntimeExplorerTree: action, reprocessCurrentTabState: action, }); this.explorerTreeData = getRuntimeExplorerTreeData(this.runtimeValue, this.state.editorStore); this.openTabFor(this.runtimeValue); // open runtime tab on init } setExplorerTreeData(treeData) { this.explorerTreeData = treeData; } addMapping(mapping) { if (!this.runtimeValue.mappings.map((m) => m.value).includes(mapping)) { runtime_addMapping(this.runtimeValue, PackageableElementExplicitReference.create(mapping)); decorateRuntimeWithNewMapping(this.runtimeValue, mapping, this.editorStore); this.reprocessRuntimeExplorerTree(); } } deleteMapping(mappingRef) { runtime_deleteMapping(this.runtimeValue, mappingRef); this.reprocessRuntimeExplorerTree(); } changeMapping(mappingRef, newVal) { packageableElementReference_setValue(mappingRef, newVal); decorateRuntimeWithNewMapping(this.runtimeValue, newVal, this.editorStore); this.reprocessRuntimeExplorerTree(); } addIdentifiedConnection(identifiedConnection) { runtime_addIdentifiedConnection(this.runtimeValue, identifiedConnection, this.editorStore.changeDetectionState.observerContext); const connectionValue = identifiedConnection.connection instanceof ConnectionPointer ? identifiedConnection.connection.packageableConnection.value .connectionValue : identifiedConnection.connection; const el = connectionValue instanceof JsonModelConnection || connectionValue instanceof XmlModelConnection ? connectionValue.class.value : connectionValue.store?.value; if (el) { this.openTabFor(el); } if (this.currentTabState instanceof IdentifiedConnectionsEditorTabState) { this.currentTabState.openIdentifiedConnection(identifiedConnection); } this.reprocessRuntimeExplorerTree(); } openTabFor(tabElement) { if (tabElement instanceof Runtime) { if (!(this.currentTabState instanceof RuntimeEditorRuntimeTabState)) { this.currentTabState = new RuntimeEditorRuntimeTabState(this.editorStore, this); } } else if (tabElement instanceof ModelStore) { return; } else if (tabElement instanceof Class) { if (!(this.currentTabState instanceof IdentifiedConnectionsPerClassEditorTabState) || this.currentTabState.class !== tabElement) { this.currentTabState = new IdentifiedConnectionsPerClassEditorTabState(this.editorStore, this, tabElement); } } else if (tabElement instanceof Store) { if (!(this.currentTabState instanceof IdentifiedConnectionsPerStoreEditorTabState) || this.currentTabState.store !== tabElement) { this.currentTabState = new IdentifiedConnectionsPerStoreEditorTabState(this.editorStore, this, tabElement); } } } decorateRuntimeConnections() { getStoresFromMappings(this.runtimeValue.mappings.map((mapping) => mapping.value), this.editorStore).forEach((store) => runtime_addUniqueStoreConnectionsForStore(this.runtimeValue, store, this.editorStore.changeDetectionState.observerContext)); } cleanUpDecoration() { this.runtimeValue.connections = this.runtimeValue.connections.filter((storeConnections) => !isStubbed_StoreConnections(storeConnections)); } onExplorerTreeNodeExpand = (node) => { if (node.childrenIds?.length) { node.isOpen = !node.isOpen; } this.setExplorerTreeData({ ...this.explorerTreeData }); }; onExplorerTreeNodeSelect = (node) => { this.openTabFor(node.data); this.setExplorerTreeData({ ...this.explorerTreeData }); }; isTreeNodeSelected = (node) => { if (node.data instanceof Runtime) { return this.currentTabState instanceof RuntimeEditorRuntimeTabState; } else if (node.data instanceof Class) { return (this.currentTabState instanceof IdentifiedConnectionsPerClassEditorTabState && this.currentTabState.class === node.data); } else if (node.data instanceof Store && !(node.data instanceof ModelStore)) { return (this.currentTabState instanceof IdentifiedConnectionsPerStoreEditorTabState && this.currentTabState.store === node.data); } return false; }; getExplorerTreeChildNodes = (node) => { if (!node.childrenIds) { return []; } return node.childrenIds .map((id) => this.explorerTreeData.nodes.get(id)) .filter(isNonNullable); }; reprocessRuntimeExplorerTree() { const openedTreeNodeIds = Array.from(this.explorerTreeData.nodes.values()) .filter((node) => node.isOpen) .map((node) => node.id); const treeData = getRuntimeExplorerTreeData(this.runtimeValue, this.editorStore); openedTreeNodeIds.forEach((nodeId) => { const node = treeData.nodes.get(nodeId); if (node && !node.isOpen) { node.isOpen = true; } }); this.setExplorerTreeData(treeData); } /** * If the currently opened connection tab is a connection pointer whose store/source class has been changed, we will * remove this tab and switch to default runtime tab */ reprocessCurrentTabState() { if (this.currentTabState instanceof IdentifiedConnectionsEditorTabState) { const connection = this.currentTabState.identifiedConnectionEditorState ?.connectionEditorState.connection; const connectionValue = connection instanceof ConnectionPointer ? connection.packageableConnection.value.connectionValue : connection; if (this.currentTabState instanceof IdentifiedConnectionsPerClassEditorTabState) { if (((connectionValue instanceof JsonModelConnection || connectionValue instanceof XmlModelConnection) && connectionValue.class.value !== this.currentTabState.class) || (this.currentTabState instanceof IdentifiedConnectionsPerStoreEditorTabState && this.currentTabState.store !== connectionValue?.store?.value)) { this.openTabFor(this.runtimeValue); } } } } } export var LakehouseRuntimeType; (function (LakehouseRuntimeType) { LakehouseRuntimeType["ENVIRONMENT"] = "ENVIRONMENT"; LakehouseRuntimeType["CONNECTION"] = "CONNECTION"; })(LakehouseRuntimeType || (LakehouseRuntimeType = {})); export class LakehouseRuntimeEditorState extends EngineRuntimeEditorState { availableEnvs; lakehouseRuntimeType = LakehouseRuntimeType.ENVIRONMENT; constructor(state, value) { super(state, value); makeObservable(this, { availableEnvs: observable, fetchLakehouseSummaries: flow, setEnvSummaries: action, lakehouseRuntimeType: observable, setLakehouseRuntimeType: action, envOptions: computed, }); this.runtimeValue = value; // fix when metamodel is more clear on this if (value.connectionPointer) { this.lakehouseRuntimeType = LakehouseRuntimeType.CONNECTION; } } setLakehouseRuntimeType(val) { if (val !== this.lakehouseRuntimeType) { this.lakehouseRuntimeType = val; if (val === LakehouseRuntimeType.CONNECTION) { this.runtimeValue.environment = undefined; this.runtimeValue.warehouse = undefined; } else { this.setConnection(undefined); } } } setConnection(val) { lakehouseRuntime_setConnection(this.runtimeValue, val); } get envOptions() { return this.availableEnvs?.map((e) => this.convertEnvToOption(e)) ?? []; } convertEnvToOption(val) { const discoveryUrlSuffix = this.editorStore.applicationStore.config.options.ingestDeploymentConfig ?.discoveryUrlSuffix; const host = new URL(val.ingestServerUrl).host; const value = discoveryUrlSuffix ? removeSuffix(host, discoveryUrlSuffix) : host; return { label: value, value, }; } setEnvSummaries(val) { this.availableEnvs = val; } *fetchLakehouseSummaries(token) { try { const ingestionManager = this.editorStore.ingestionManager; this.setEnvSummaries(undefined); if (ingestionManager) { const res = (yield ingestionManager.fetchLakehouseEnvironmentSummaries(token)); this.setEnvSummaries(res); if (this.lakehouseRuntimeType === LakehouseRuntimeType.ENVIRONMENT && !this.runtimeValue.environment && this.envOptions.length) { this.runtimeValue.environment = this.envOptions[0]?.value; } } } catch (error) { assertErrorThrown(error); } } } export class RuntimeEditorState { /** * NOTE: used to force component remount on state change */ uuid = uuid(); editorStore; runtime; runtimeValueEditorState; isEmbeddedRuntime; constructor(editorStore, runtime, isEmbeddedRuntime) { makeObservable(this, { runtimeValueEditorState: observable, }); this.editorStore = editorStore; this.runtime = runtime; this.isEmbeddedRuntime = isEmbeddedRuntime; const runtimeValue = runtime instanceof RuntimePointer ? runtime.packageableRuntime.value.runtimeValue : guaranteeType(runtime, EngineRuntime); this.runtimeValueEditorState = runtimeValue instanceof LakehouseRuntime ? new LakehouseRuntimeEditorState(this, runtimeValue) : new EngineRuntimeEditorState(this, runtimeValue); } } export class PackageableRuntimeEditorState extends ElementEditorState { runtimeEditorState; constructor(editorStore, packageableRuntime) { super(editorStore, packageableRuntime); makeObservable(this, { runtime: computed, reprocess: action, }); this.runtimeEditorState = new RuntimeEditorState(editorStore, this.runtime.runtimeValue, false); } get runtime() { return guaranteeType(this.element, PackageableRuntime, 'Element inside runtime editor state must be a packageable runtime'); } reprocess(newElement, editorStore) { const editorState = new PackageableRuntimeEditorState(editorStore, newElement); return editorState; } } //# sourceMappingURL=RuntimeEditorState.js.map