mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-16 14:50:15 +08:00
Merge branch 'devel' into dbeaver/pro#5476-task-scheduler-refactor
This commit is contained in:
+6
-1
@@ -41,6 +41,7 @@ import org.jkiss.dbeaver.utils.GeneralUtils;
|
||||
import org.jkiss.dbeaver.utils.RuntimeUtils;
|
||||
import org.jkiss.utils.CommonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
@@ -204,7 +205,7 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
|
||||
initializeApplicationServices();
|
||||
try {
|
||||
try {
|
||||
this.instanceServer = new CloudBeaverInstanceServer();
|
||||
this.instanceServer = createInstanceServer();
|
||||
} catch (Exception e) {
|
||||
log.error("Error initializing instance server", e);
|
||||
}
|
||||
@@ -216,6 +217,10 @@ public abstract class BaseServletApplication extends BaseApplicationImpl impleme
|
||||
return EXIT_OK;
|
||||
}
|
||||
|
||||
protected CloudBeaverInstanceServer createInstanceServer() throws IOException {
|
||||
return new CloudBeaverInstanceServer();
|
||||
}
|
||||
|
||||
protected abstract void startServer() throws DBException;
|
||||
|
||||
@Override
|
||||
|
||||
+29
-8
@@ -16,20 +16,41 @@
|
||||
*/
|
||||
package io.cloudbeaver.model.cli;
|
||||
|
||||
import org.jkiss.dbeaver.model.cli.ApplicationCommandLine;
|
||||
import org.jkiss.dbeaver.model.cli.ApplicationInstanceController;
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.model.cli.*;
|
||||
import org.jkiss.dbeaver.model.cli.model.CommandLineAuthenticator;
|
||||
import picocli.CommandLine;
|
||||
|
||||
public class CloudBeaverCommandLine extends ApplicationCommandLine<ApplicationInstanceController> {
|
||||
private static CloudBeaverCommandLine INSTANCE = null;
|
||||
|
||||
private CloudBeaverCommandLine() {
|
||||
@Nullable
|
||||
private final CommandLineAuthenticator authenticator;
|
||||
|
||||
public CloudBeaverCommandLine(@Nullable CommandLineAuthenticator authenticator) {
|
||||
super();
|
||||
this.authenticator = authenticator;
|
||||
}
|
||||
|
||||
public synchronized static CloudBeaverCommandLine getInstance() {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new CloudBeaverCommandLine();
|
||||
@Override
|
||||
protected CloudBeaverTopLevelCommand createTopLevelCommand(
|
||||
@Nullable ApplicationInstanceController applicationInstanceController,
|
||||
@NotNull CommandLineContext context,
|
||||
@NotNull CLIRunMeta runMeta
|
||||
) {
|
||||
if (authenticator != null) {
|
||||
context.setContextParameter(CLIConstants.CONTEXT_PARAM_AUTHENTICATOR, authenticator);
|
||||
}
|
||||
return INSTANCE;
|
||||
return new CloudBeaverTopLevelCommand(applicationInstanceController, context, runMeta);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected CommandLine initCommandLine(
|
||||
@Nullable ApplicationInstanceController applicationInstanceController,
|
||||
@NotNull CommandLineContext context,
|
||||
@NotNull CLIRunMeta runMeta
|
||||
) {
|
||||
return super.initCommandLine(applicationInstanceController, context, runMeta);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-5
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
package io.cloudbeaver.model.cli;
|
||||
|
||||
import org.apache.commons.cli.CommandLine;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.dbeaver.model.cli.ApplicationInstanceController;
|
||||
import org.jkiss.dbeaver.model.cli.ApplicationInstanceServer;
|
||||
@@ -25,20 +25,27 @@ import org.jkiss.dbeaver.model.cli.CLIProcessResult;
|
||||
import java.io.IOException;
|
||||
|
||||
public class CloudBeaverInstanceServer extends ApplicationInstanceServer<ApplicationInstanceController> {
|
||||
private final CloudBeaverCommandLine commandLine;
|
||||
|
||||
public CloudBeaverInstanceServer() throws IOException {
|
||||
super(ApplicationInstanceController.class);
|
||||
this.commandLine = new CloudBeaverCommandLine(null);
|
||||
}
|
||||
|
||||
protected CloudBeaverInstanceServer(@NotNull CloudBeaverCommandLine commandLine) throws IOException {
|
||||
super(ApplicationInstanceController.class);
|
||||
this.commandLine = commandLine;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public CLIProcessResult handleCommandLine(@NotNull String[] args) {
|
||||
CommandLine cmd = CloudBeaverCommandLine.getInstance().getCommandLine(args);
|
||||
try {
|
||||
return CloudBeaverCommandLine.getInstance().executeCommandLineCommands(
|
||||
cmd,
|
||||
return commandLine.executeCommandLineCommands(
|
||||
this,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
args
|
||||
);
|
||||
} catch (Exception e) {
|
||||
return new CLIProcessResult(CLIProcessResult.PostAction.ERROR, "Error executing command: " + e.getMessage());
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2025 DBeaver Corp and others
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package io.cloudbeaver.model.cli;
|
||||
|
||||
import org.jkiss.code.NotNull;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.model.cli.ApplicationInstanceController;
|
||||
import org.jkiss.dbeaver.model.cli.CLIRunMeta;
|
||||
import org.jkiss.dbeaver.model.cli.CommandLineContext;
|
||||
import org.jkiss.dbeaver.model.cli.command.AbstractTopLevelCommand;
|
||||
import picocli.CommandLine;
|
||||
|
||||
@CommandLine.Command(name = "cbvr", description = "CloudBeaver commands", mixinStandardHelpOptions = true)
|
||||
public class CloudBeaverTopLevelCommand extends AbstractTopLevelCommand {
|
||||
protected CloudBeaverTopLevelCommand(
|
||||
@Nullable ApplicationInstanceController controller,
|
||||
@NotNull CommandLineContext context,
|
||||
@NotNull CLIRunMeta meta
|
||||
|
||||
) {
|
||||
super(controller, context, meta);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -254,8 +254,12 @@ public abstract class BaseWebSession extends AbstractSessionPersistent {
|
||||
@Property
|
||||
public long getRemainingTime() {
|
||||
if (application instanceof ServletAuthApplication authApplication) {
|
||||
return authApplication.getMaxSessionIdleTime() + lastAccessTime - System.currentTimeMillis();
|
||||
return getMaxSessionIdleTime(authApplication) + lastAccessTime - System.currentTimeMillis();
|
||||
}
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
protected long getMaxSessionIdleTime(@NotNull ServletAuthApplication authApplication) {
|
||||
return authApplication.getMaxSessionIdleTime();
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -190,6 +190,10 @@ public class WebUserContext implements SMCredentialsProvider {
|
||||
return isAuthorizedInSecurityManager() && getUser() != null;
|
||||
}
|
||||
|
||||
public synchronized boolean isAnonymousUserAuthorizedInSM() {
|
||||
return isAuthorizedInSecurityManager() && getUser() == null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public synchronized WebUser getUser() {
|
||||
return user;
|
||||
|
||||
+1
-4
@@ -304,14 +304,11 @@ public class CBSessionManager implements WebAppSessionManager {
|
||||
}
|
||||
|
||||
public void expireIdleSessions() {
|
||||
long maxSessionIdleTime = application.getMaxSessionIdleTime();
|
||||
|
||||
List<BaseWebSession> expiredList = new ArrayList<>();
|
||||
synchronized (sessionMap) {
|
||||
for (Iterator<BaseWebSession> iterator = sessionMap.values().iterator(); iterator.hasNext(); ) {
|
||||
var session = iterator.next();
|
||||
long idleMillis = System.currentTimeMillis() - session.getLastAccessTimeMillis();
|
||||
if (idleMillis >= maxSessionIdleTime) {
|
||||
if (!session.isValid()) {
|
||||
iterator.remove();
|
||||
expiredList.add(session);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import type { DialogComponentProps } from '@cloudbeaver/core-dialogs';
|
||||
|
||||
import { observable } from 'mobx';
|
||||
import { EXPORT_IMAGE_FORMATS, type ExportImageFormat } from './ExportImageFormats.js';
|
||||
import { useTranslate } from '../localization/useTranslate.js';
|
||||
import { useObservableRef } from '../useObservableRef.js';
|
||||
import { CommonDialogBody } from '../CommonDialog/CommonDialog/CommonDialogBody.js';
|
||||
import { CommonDialogFooter } from '../CommonDialog/CommonDialog/CommonDialogFooter.js';
|
||||
import { CommonDialogHeader } from '../CommonDialog/CommonDialog/CommonDialogHeader.js';
|
||||
import { CommonDialogWrapper } from '../CommonDialog/CommonDialog/CommonDialogWrapper.js';
|
||||
import { Container } from '../Containers/Container.js';
|
||||
import { ErrorMessage } from '../ErrorMessage.js';
|
||||
import { FieldCheckbox } from '../FormControls/Checkboxes/FieldCheckbox.js';
|
||||
import { InputField } from '../FormControls/InputField/InputField.js';
|
||||
import { useErrorDetails } from '../useErrorDetails.js';
|
||||
import { Select } from '../FormControls/Select.js';
|
||||
import { Button } from '../Button.js';
|
||||
|
||||
export interface IExportImageOptions {
|
||||
format: ExportImageFormat;
|
||||
transparent: boolean;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface ExportImagePayload {
|
||||
export: (options: IExportImageOptions) => Promise<void>;
|
||||
defaultFileName: string;
|
||||
}
|
||||
|
||||
interface State extends IExportImageOptions {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export const ExportImageDialog = observer<DialogComponentProps<ExportImagePayload, null>>(function ExportImageDialog(props) {
|
||||
const translate = useTranslate();
|
||||
const state = useObservableRef<State>(
|
||||
() => ({
|
||||
format: 'SVG',
|
||||
transparent: false,
|
||||
fileName: props.payload.defaultFileName,
|
||||
error: null,
|
||||
}),
|
||||
{
|
||||
format: observable.ref,
|
||||
transparent: observable.ref,
|
||||
fileName: observable.ref,
|
||||
error: observable.ref,
|
||||
},
|
||||
false,
|
||||
);
|
||||
const errorDetails = useErrorDetails(state.error);
|
||||
|
||||
async function onExportHandler() {
|
||||
try {
|
||||
await props.payload.export(state);
|
||||
props.resolveDialog();
|
||||
} catch (error: any) {
|
||||
state.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<CommonDialogWrapper size="small">
|
||||
<CommonDialogHeader title="core_blocks_export_image_dialog_title" onReject={props.rejectDialog} />
|
||||
<CommonDialogBody>
|
||||
<Container>
|
||||
<Container small gap>
|
||||
<InputField state={state} name="fileName">
|
||||
{translate('ui_name')}
|
||||
</InputField>
|
||||
<Select items={EXPORT_IMAGE_FORMATS} keySelector={value => value} valueSelector={value => value} state={state} name="format">
|
||||
{translate('core_blocks_export_image_dialog_format')}
|
||||
</Select>
|
||||
<FieldCheckbox state={state} name="transparent" label={translate('core_blocks_export_image_dialog_transparent_background')} />
|
||||
</Container>
|
||||
</Container>
|
||||
{errorDetails.name && (
|
||||
<ErrorMessage text={errorDetails.message || errorDetails.name} hasDetails={errorDetails.hasDetails} onShowDetails={errorDetails.open} />
|
||||
)}
|
||||
</CommonDialogBody>
|
||||
<CommonDialogFooter className="tw:justify-end tw:items-center tw:gap-[24px]">
|
||||
<Button type="button" variant="secondary" onClick={props.rejectDialog}>
|
||||
{translate('app_shared_inlineEditor_dialog_cancel')}
|
||||
</Button>
|
||||
<Button type="button" onClick={onExportHandler}>
|
||||
{translate('ui_export')}
|
||||
</Button>
|
||||
</CommonDialogFooter>
|
||||
</CommonDialogWrapper>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { importLazyComponent } from '../importLazyComponent.js';
|
||||
|
||||
export const ExportImageDialog = importLazyComponent(() => import('./ExportImageDialog.js').then(m => m.ExportImageDialog));
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export type ExportImageFormat = 'SVG' | 'PNG';
|
||||
|
||||
export const EXPORT_IMAGE_FORMATS: ExportImageFormat[] = ['SVG', 'PNG'];
|
||||
@@ -19,6 +19,8 @@ export { default as ConfirmationDialogStyles } from './CommonDialog/Confirmation
|
||||
export * from './CommonDialog/ConfirmationDialogDelete.js';
|
||||
export * from './CommonDialog/RenameDialog.js';
|
||||
export * from './CommonDialog/DialogsPortal.js';
|
||||
export * from './ExportImageDialog/ExportImageDialogLazy.js';
|
||||
export * from './ExportImageDialog/ExportImageFormats.js';
|
||||
|
||||
export * from './ErrorDetailsDialog/ErrorDetailsDialog.js';
|
||||
|
||||
|
||||
@@ -13,4 +13,7 @@ export default [
|
||||
['core_blocks_exception_message_error_title', 'Unable to display content'],
|
||||
['core_blocks_exception_message_error_message', 'Something went wrong.'],
|
||||
['core_blocks_custom_input_validation_error', 'Field value is not valid'],
|
||||
['core_blocks_export_image_dialog_title', 'Export Image'],
|
||||
['core_blocks_export_image_dialog_format', 'File format'],
|
||||
['core_blocks_export_image_dialog_transparent_background', 'Transparent background'],
|
||||
];
|
||||
|
||||
@@ -12,4 +12,7 @@ export default [
|
||||
['core_blocks_exception_message_error_title', "Impossible d'afficher le contenu"],
|
||||
['core_blocks_exception_message_error_message', 'Quelque chose a mal tourné.'],
|
||||
['core_blocks_custom_input_validation_error', "La valeur du champ n'est pas valide"],
|
||||
['core_blocks_export_image_dialog_title', "Exporter l'image"],
|
||||
['core_blocks_export_image_dialog_format', 'Format de fichier'],
|
||||
['core_blocks_export_image_dialog_transparent_background', 'Fond transparent'],
|
||||
];
|
||||
|
||||
@@ -7,4 +7,7 @@ export default [
|
||||
['core_blocks_object_property_info_form_empty_placeholder', 'Properties empty'],
|
||||
['core_blocks_properties_table_item_remove', 'Remove property'],
|
||||
['core_blocks_custom_input_validation_error', 'Field value is not valid'],
|
||||
['core_blocks_export_image_dialog_title', 'Esporta immagine'],
|
||||
['core_blocks_export_image_dialog_format', 'Formato file'],
|
||||
['core_blocks_export_image_dialog_transparent_background', 'Sfondo trasparente'],
|
||||
];
|
||||
|
||||
@@ -10,4 +10,7 @@ export default [
|
||||
['combobox_no_results_placeholder', 'Нет совпадений'],
|
||||
['combobox_select_placeholder', 'Выберите опцию'],
|
||||
['core_blocks_custom_input_validation_error', 'Значение не валидно'],
|
||||
['core_blocks_export_image_dialog_title', 'Экспорт изображения'],
|
||||
['core_blocks_export_image_dialog_format', 'Формат файла'],
|
||||
['core_blocks_export_image_dialog_transparent_background', 'Прозрачный фон'],
|
||||
];
|
||||
|
||||
@@ -13,4 +13,7 @@ export default [
|
||||
['core_blocks_exception_message_error_title', 'Không thể hiển thị nội dung'],
|
||||
['core_blocks_exception_message_error_message', 'Đã xảy ra lỗi.'],
|
||||
['core_blocks_custom_input_validation_error', 'Giá trị trường không hợp lệ'],
|
||||
['core_blocks_export_image_dialog_title', 'Xuất hình ảnh'],
|
||||
['core_blocks_export_image_dialog_format', 'Định dạng tệp'],
|
||||
['core_blocks_export_image_dialog_transparent_background', 'Nền trong suốt'],
|
||||
];
|
||||
|
||||
@@ -13,4 +13,7 @@ export default [
|
||||
['core_blocks_exception_message_error_title', '无法显示内容'],
|
||||
['core_blocks_exception_message_error_message', '出现错误.'],
|
||||
['core_blocks_custom_input_validation_error', '字段值无效'],
|
||||
['core_blocks_export_image_dialog_title', '导出图像'],
|
||||
['core_blocks_export_image_dialog_format', '文件格式'],
|
||||
['core_blocks_export_image_dialog_transparent_background', '透明背景'],
|
||||
];
|
||||
|
||||
@@ -12,7 +12,7 @@ module.exports = [
|
||||
'joint',
|
||||
'backbone',
|
||||
'lodash',
|
||||
'canvg',
|
||||
'modern-screenshot',
|
||||
'dagre',
|
||||
'svg-pathdata',
|
||||
'@emotion',
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"fast-deep-equal": "^3",
|
||||
"md5": "^2",
|
||||
"mobx": "^6",
|
||||
"modern-screenshot": "^4",
|
||||
"tslib": "^2",
|
||||
"underscore": "^1",
|
||||
"uuid": "^13",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import * as modernScreenshot from 'modern-screenshot';
|
||||
import type { Options } from 'modern-screenshot';
|
||||
import { download } from './download.js';
|
||||
import { isImageBroken } from './isBrokenImage.js';
|
||||
|
||||
export type { Options as IScreenshotOptions };
|
||||
|
||||
const BROKEN_IMAGE_ERROR_MESSAGE = 'Something went wrong. please try select another file format';
|
||||
|
||||
export async function downloadSvg<T extends Node>(element: T, options: Options, fileName: string): Promise<void> {
|
||||
const svg = await modernScreenshot.domToForeignObjectSvg<T>(element, options);
|
||||
const svgData = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob(['<?xml version="1.0" standalone="no"?>\r\n', svgData], { type: 'image/svg+xml;charset=utf-8' });
|
||||
const uri = URL.createObjectURL(blob);
|
||||
const isBrokenImage = await isImageBroken(uri);
|
||||
|
||||
if (isBrokenImage) {
|
||||
throw new Error(BROKEN_IMAGE_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
download(blob, `${fileName}.svg`);
|
||||
}
|
||||
|
||||
export async function downloadPng<T extends Node>(element: T, options: Options, fileName: string): Promise<void> {
|
||||
const png = await modernScreenshot.domToPng(element, options);
|
||||
const isBrokenImage = await isImageBroken(png);
|
||||
|
||||
if (isBrokenImage) {
|
||||
throw new Error(BROKEN_IMAGE_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
download(png, `${fileName}.png`);
|
||||
}
|
||||
@@ -89,3 +89,4 @@ export * from './bindFunctions.js';
|
||||
export * from './getDomainFromUrl.js';
|
||||
export * from './isNumber.js';
|
||||
export * from './getSubjectDifferences.js';
|
||||
export * from './downloadImage.js';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export function isImageBroken(imageUrl: string): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
img.src = imageUrl;
|
||||
});
|
||||
}
|
||||
@@ -2230,6 +2230,7 @@ __metadata:
|
||||
fast-deep-equal: "npm:^3"
|
||||
md5: "npm:^2"
|
||||
mobx: "npm:^6"
|
||||
modern-screenshot: "npm:^4"
|
||||
rimraf: "npm:^6"
|
||||
tslib: "npm:^2"
|
||||
typescript: "npm:^5"
|
||||
@@ -15160,6 +15161,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"modern-screenshot@npm:^4":
|
||||
version: 4.6.6
|
||||
resolution: "modern-screenshot@npm:4.6.6"
|
||||
checksum: 10c0/0632cdd89db1a7c0b2d8e78d3e05daca77a62d14ee172f6480da3b2de97fd37938c6ed71041271014a601fe2bcb470d01a78c10456ef7ac59a8674b82c64d0a1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ms@npm:^2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "ms@npm:2.1.3"
|
||||
|
||||
Reference in New Issue
Block a user