From a2d9fddc5e853f1fc925c3f1ac8414674857c9d4 Mon Sep 17 00:00:00 2001 From: Alexander Skoblikov Date: Wed, 27 Sep 2023 15:30:37 +0200 Subject: [PATCH 1/2] CB-3930 redesign resource management api (#1998) * CB-3946 rm nio api * CB-3946 read/write nio api * CB-3946 nio tests --- .../io.cloudbeaver.model/META-INF/MANIFEST.MF | 2 + .../model/rm/fs/nio/RMByteArrayChannel.java | 77 +++++ .../model/rm/fs/nio/RMNIOFileSystem.java | 149 +++++++++ .../rm/fs/nio/RMNIOFileSystemProvider.java | 293 ++++++++++++++++++ .../rm/fs/nio/RMNIOProjectFileStore.java | 50 +++ .../rm/fs/nio/RMNIOResourceFileStore.java | 50 +++ .../model/rm/fs/nio/RMOutputStream.java | 46 +++ .../cloudbeaver/model/rm/fs/nio/RMPath.java | 205 ++++++++++++ .../rm/fs/nio/RMResourceBasicAttribute.java | 48 +++ .../model/rm/fs/nio/RMRootBasicAttribute.java | 38 +++ .../rm/local/LocalResourceController.java | 6 + .../server/jobs/SessionStateJob.java | 2 +- .../io/cloudbeaver/model/rm/RMNIOTest.java | 177 +++++++++++ .../test/platform/CEServerTestSuite.java | 4 +- 14 files changed, 1145 insertions(+), 2 deletions(-) create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMByteArrayChannel.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystem.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystemProvider.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOProjectFileStore.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOResourceFileStore.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMOutputStream.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMPath.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMResourceBasicAttribute.java create mode 100644 server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMRootBasicAttribute.java create mode 100644 server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/RMNIOTest.java diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index 2469bde4a7..3ed724a284 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -12,6 +12,7 @@ Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport, org.jkiss.dbeaver.model;visibility:=reexport, org.jkiss.dbeaver.model.sm;visibility:=reexport, org.jkiss.dbeaver.model.event;visibility:=reexport, + org.jkiss.dbeaver.model.nio;visibility:=reexport, org.jkiss.dbeaver.registry;visibility:=reexport, org.jkiss.dbeaver.net.ssh, org.jkiss.bundle.graphql.java;visibility:=reexport, @@ -29,6 +30,7 @@ Export-Package: io.cloudbeaver, io.cloudbeaver.model.rm, io.cloudbeaver.model.rm.local, io.cloudbeaver.model.rm.lock, + io.cloudbeaver.model.rm.fs.nio, io.cloudbeaver.model.session, io.cloudbeaver.model.user, io.cloudbeaver.registry, diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMByteArrayChannel.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMByteArrayChannel.java new file mode 100644 index 0000000000..d0f6913f7f --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMByteArrayChannel.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package io.cloudbeaver.model.rm.fs.nio; + +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.nio.ByteArrayChannel; + +import java.io.IOException; +import java.nio.file.OpenOption; +import java.util.Set; + +// copy of jdk.nio.zipfs.ByteArrayChannel +public class RMByteArrayChannel extends ByteArrayChannel { + + private final RMPath rmPath; + + public RMByteArrayChannel(byte[] buf, RMPath rmPath, Set options) { + super(buf, options); + this.rmPath = rmPath; + } + + @Override + protected void createNewFile() throws IOException { + try { + rmPath.getFileSystem().getRmController().createResource( + rmPath.getRmProjectId(), rmPath.getResourcePath(), false + ); + } catch (DBException e) { + throw new IOException("Failed to create new file: " + e.getMessage(), e); + } + } + + @Override + protected void writeToFile() throws IOException { + try { + rmPath.getFileSystem().getRmController().setResourceContents( + rmPath.getRmProjectId(), rmPath.getResourcePath(), buf, true + ); + } catch (DBException e) { + throw new IOException("Failed to write data to the file: " + e.getMessage(), e); + } + } + + @Override + protected void deleteFile() throws IOException { + try { + rmPath.getFileSystem().getRmController().deleteResource( + rmPath.getRmProjectId(), rmPath.getResourcePath(), true + ); + } catch (DBException e) { + throw new IOException("Failed to delete file: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystem.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystem.java new file mode 100644 index 0000000000..0abc646d75 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystem.java @@ -0,0 +1,149 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.rm.fs.nio; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.nio.NIOFileSystem; +import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.rm.RMProjectPermission; +import org.jkiss.utils.ArrayUtils; +import org.jkiss.utils.CommonUtils; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileStore; +import java.nio.file.Path; +import java.nio.file.spi.FileSystemProvider; +import java.util.List; + +public class RMNIOFileSystem extends NIOFileSystem { + private static final Log log = Log.getLog(RMNIOFileSystem.class); + @Nullable + // null for root 'rm://' path + private final String rmProjectId; + @NotNull + private final RMController rmController; + @NotNull + private final RMNIOFileSystemProvider rmNioFileSystemProvider; + @Nullable + private RMProject rmProject; + + public RMNIOFileSystem( + @Nullable String rmProjectId, + @NotNull RMController rmController, + @NotNull RMNIOFileSystemProvider rmNioFileSystemProvider + ) { + this.rmProjectId = rmProjectId; + this.rmController = rmController; + this.rmNioFileSystemProvider = rmNioFileSystemProvider; + } + + @Override + public FileSystemProvider provider() { + return rmNioFileSystemProvider; + } + + @Override + public void close() throws IOException { + + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public boolean isReadOnly() { + try { + return getRmProject().hasProjectPermission(RMProjectPermission.RESOURCE_EDIT.getPermissionId()); + } catch (IOException e) { + return true; + } + } + + @Override + public Iterable getRootDirectories() { + return List.of(new RMPath(this)); + } + + @Override + public Iterable getFileStores() { + try { + return List.of(new RMNIOProjectFileStore(getRmProject())); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return List.of(); + } + + @NotNull + @Override + public Path getPath(@NotNull String first, @NotNull String... more) { + if (CommonUtils.isEmpty(first)) { + throw new IllegalArgumentException("Empty path"); + } + StringBuilder uriBuilder = new StringBuilder(); + uriBuilder.append(provider().getScheme()).append("://"); + if (rmProjectId != null) { + uriBuilder.append(rmProjectId).append(getSeparator()); + + } + uriBuilder.append(first); + if (!ArrayUtils.isEmpty(more)) { + uriBuilder + .append(getSeparator()) + .append(String.join(getSeparator(), more)); + } + return provider().getPath(URI.create(uriBuilder.toString())); + } + + @NotNull + public RMController getRmController() { + return rmController; + } + + @Nullable + public String getRmProjectId() { + return rmProjectId; + } + + @NotNull + public synchronized RMProject getRmProject() throws IOException { + if (rmProject != null) { + return rmProject; + } + if (rmProjectId == null) { + throw new IOException("Project id not specified"); + } + try { + RMProject project = rmController.getProject(rmProjectId, false, false); + if (project == null) { + throw new FileNotFoundException("Project not exist: " + rmProjectId); + } + rmProject = project; + return rmProject; + } catch (DBException e) { + throw new IOException("Failed to get project:" + e.getMessage(), e); + } + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystemProvider.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystemProvider.java new file mode 100644 index 0000000000..e87da418fa --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystemProvider.java @@ -0,0 +1,293 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.rm.fs.nio; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.nio.NIOFileSystemProvider; +import org.jkiss.dbeaver.model.nio.NIOUtils; +import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.rm.RMResource; +import org.jkiss.dbeaver.model.rm.RMUtils; +import org.jkiss.utils.CommonUtils; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileAttributeView; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +public class RMNIOFileSystemProvider extends NIOFileSystemProvider { + + @NotNull + private final RMController rmController; + + public RMNIOFileSystemProvider(@NotNull RMController rmController) { + this.rmController = rmController; + } + + @Override + public String getScheme() { + return "rm"; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + throw new FileAlreadyExistsException("RM file system already exist"); + } + + @Override + public FileSystem getFileSystem(URI uri) { + validateUri(uri); + String projectId = uri.getAuthority(); + if (CommonUtils.isEmpty(projectId)) { + projectId = null; + } + try { + return new RMNIOFileSystem(projectId, rmController, this); + } catch (Exception e) { + throw new FileSystemNotFoundException("RM file system not found: " + e.getMessage()); + } + } + + @Override + public Path getPath(URI uri) { + validateUri(uri); + String projectId = uri.getAuthority(); + if (CommonUtils.isEmpty(projectId)) { + projectId = null; + } + RMNIOFileSystem rmNioFileSystem = new RMNIOFileSystem(projectId, rmController, this); + String resourcePath = uri.getPath(); + if (CommonUtils.isNotEmpty(resourcePath) && projectId == null) { + throw new IllegalArgumentException("Project is not specified in URI"); + } + if (CommonUtils.isEmpty(resourcePath)) { + return new RMPath(rmNioFileSystem); + } else { + return new RMPath(rmNioFileSystem, resourcePath); + } + } + + @Override + public RMByteArrayChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { + RMPath rmPath = (RMPath) path; + if (rmPath.isProjectPath()) { + throw new IllegalArgumentException("Cannot open channel for the project"); + } + + try { + if (Files.exists(path)) { + byte[] data = rmController.getResourceContents(rmPath.getRmProjectId(), rmPath.getResourcePath()); + return new RMByteArrayChannel(data, rmPath, options); + } else { + return new RMByteArrayChannel(new byte[0], rmPath, options); + } + } catch (DBException e) { + throw new IOException("Failed to read resource: " + e.getMessage(), e); + } + } + + @Override + public OutputStream newOutputStream(Path path, OpenOption... options) throws IOException { + return new RMOutputStream((RMPath) path); + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { + RMPath rmDir = (RMPath) dir; + String rmDirPath = rmDir.getResourcePath(); + String separator = rmDir.getFileSystem().getSeparator(); + + return new DirectoryStream<>() { + @Override + public Iterator iterator() { + var rmController = rmDir.getFileSystem().getRmController(); + try { + if (rmDir.isRmRootPath()) { + return Arrays.stream(rmController.listAccessibleProjects()) + .map(rmProject -> (Path) new RMPath( + new RMNIOFileSystem( + rmProject.getId(), + rmController, + RMNIOFileSystemProvider.this + ) + ) + ).iterator(); + } else { + var resources = rmController.listResources( + rmDir.getRmProjectId(), + rmDirPath, + null, + false, + false, + false + ); + return Arrays.stream(resources) + .map(rmResource -> (Path) new RMPath(rmDir.getFileSystem(), + NIOUtils.resolve(separator, rmDirPath, rmResource.getName()) + )) + .iterator(); + } + } catch (DBException e) { + throw new RuntimeException("Failed to read resources from rm path: " + e.getMessage(), e); + } + } + + @Override + public void close() throws IOException { + + } + }; + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + RMPath rmDir = (RMPath) dir; + try { + if (rmDir.isProjectPath()) { + rmController.createProject(RMUtils.getProjectName(rmDir.getRmProjectId()), null); + } else { + rmController.createResource(rmDir.getRmProjectId(), rmDir.getResourcePath(), true); + } + } catch (DBException e) { + throw new IOException("Failed to create directory: " + e.getMessage(), e); + } + } + + @Override + public void delete(Path path) throws IOException { + RMPath rmPath = (RMPath) path; + if (rmPath.isRmRootPath()) { + throw new IOException("Can not delete root rm directory"); + } + try { + String rmProjectId = rmPath.getRmProjectId(); + var rmController = rmPath.getFileSystem().getRmController(); + if (rmPath.isProjectPath()) { + rmController.deleteProject(rmProjectId); + } else { + rmController.deleteResource(rmProjectId, rmPath.getResourcePath(), true); + } + } catch (DBException e) { + throw new IOException("Failed to delete rm resource: " + e.getMessage(), e); + } + } + + @Override + public void copy(Path source, Path target, CopyOption... options) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void move(Path source, Path target, CopyOption... options) throws IOException { + RMPath rmSource = (RMPath) source; + RMPath rmTarget = (RMPath) target; + try { + rmController.moveResource(rmSource.getRmProjectId(), rmSource.getResourcePath(), rmTarget.getResourcePath()); + } catch (DBException e) { + throw new IOException("Failed to move rm resource: " + e.getMessage(), e); + } + } + + @Override + public boolean isSameFile(Path path, Path path2) throws IOException { + return path.toString().equals(path2.toString()); + } + + @Override + public boolean isHidden(Path path) throws IOException { + //rm does not support hidden files + return false; + } + + @Override + public FileStore getFileStore(Path path) throws IOException { + RMPath rmPath = (RMPath) path; + if (rmPath.isProjectPath()) { + return new RMNIOProjectFileStore(rmPath.getFileSystem().getRmProject()); + } else { + try { + RMResource rmResource = rmController.getResource(rmPath.getRmProjectId(), rmPath.getResourcePath()); + if (rmResource == null) { + throw new FileNotFoundException(); + } + return new RMNIOResourceFileStore(rmResource); + } catch (DBException e) { + throw new IOException("Failed to "); + } + } + } + + @Override + public void checkAccess(Path path, AccessMode... modes) throws IOException { + RMPath rmPath = (RMPath) path; + try { + if (rmPath.isProjectPath()) { + if (rmController.getProject(rmPath.getRmProjectId(), false, false) == null) { + // we should throw error, otherwise Files.exists return true + throw new FileNotFoundException("Project not found: " + rmPath.getRmProjectId()); + } + } else if (rmController.getResource(rmPath.getRmProjectId(), rmPath.getResourcePath()) == null) { + // some here + throw new FileNotFoundException("Resource not found: " + rmPath.getResourcePath()); + } + } catch (Exception e) { + throw new IOException("Failed to check path access: " + e.getMessage(), e); + } + } + + @Override + public V getFileAttributeView(Path path, Class type, LinkOption... options) { + return null; + } + + @Override + public A readAttributes(Path path, Class type, LinkOption... options) throws IOException { + if (type == BasicFileAttributes.class) { + RMPath rmPath = (RMPath) path; + try { + if (rmPath.isRmRootPath()) { + return type.cast(new RMRootBasicAttribute()); + } + if (rmPath.isProjectPath()) { + boolean projectNotExist = rmController.getProject(rmPath.getRmProjectId(), false, false) == null; + if (projectNotExist) { + throw new FileNotFoundException(); + } + return type.cast(new RMRootBasicAttribute()); + } else { + RMResource rmResource = rmController.getResource(rmPath.getRmProjectId(), rmPath.getResourcePath()); + if (rmResource == null) { + throw new FileNotFoundException(); + } + return type.cast(new RMResourceBasicAttribute(rmResource)); + } + } catch (DBException e) { + throw new IOException("Failed to read resource attribute: " + e.getMessage(), e); + } + } + return null; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOProjectFileStore.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOProjectFileStore.java new file mode 100644 index 0000000000..da1bd715f8 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOProjectFileStore.java @@ -0,0 +1,50 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.rm.fs.nio; + +import org.jkiss.dbeaver.model.nio.NIOFileStore; +import org.jkiss.dbeaver.model.rm.RMProject; + +import java.io.IOException; + +public class RMNIOProjectFileStore extends NIOFileStore { + private final RMProject rmProject; + + public RMNIOProjectFileStore(RMProject rmProject) { + this.rmProject = rmProject; + } + + @Override + public String name() { + return rmProject.getName(); + } + + @Override + public String type() { + return "rmfs"; + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public long getTotalSpace() throws IOException { + return 0; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOResourceFileStore.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOResourceFileStore.java new file mode 100644 index 0000000000..a4bc1d4c5c --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOResourceFileStore.java @@ -0,0 +1,50 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.rm.fs.nio; + +import org.jkiss.dbeaver.model.nio.NIOFileStore; +import org.jkiss.dbeaver.model.rm.RMResource; + +import java.io.IOException; + +public class RMNIOResourceFileStore extends NIOFileStore { + private final RMResource rmResource; + + public RMNIOResourceFileStore(RMResource rmResource) { + this.rmResource = rmResource; + } + + @Override + public String name() { + return rmResource.getName(); + } + + @Override + public String type() { + return "rmfs"; + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public long getTotalSpace() throws IOException { + return rmResource.getLength(); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMOutputStream.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMOutputStream.java new file mode 100644 index 0000000000..09fee299e9 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMOutputStream.java @@ -0,0 +1,46 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.rm.fs.nio; + +import org.jkiss.dbeaver.DBException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; + +public class RMOutputStream extends ByteArrayOutputStream { + private final RMPath rmPath; + + public RMOutputStream(RMPath rmPath) { + this.rmPath = rmPath; + } + + @Override + public void close() throws IOException { + try { + rmPath.getFileSystem().getRmController().setResourceContents( + rmPath.getRmProjectId(), + rmPath.getResourcePath(), + Arrays.copyOfRange(buf, 0, count), + true + ); + } catch (DBException e) { + throw new IOException("Failed to write data to the resource: " + e.getMessage(), e); + } + super.close(); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMPath.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMPath.java new file mode 100644 index 0000000000..a1e9a49c37 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMPath.java @@ -0,0 +1,205 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.rm.fs.nio; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.nio.NIOPath; +import org.jkiss.utils.CommonUtils; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Arrays; + +public class RMPath extends NIOPath { + @NotNull + private final RMNIOFileSystem rmNioFileSystem; + @Nullable + private final String rmProjectId; + + public RMPath( + @NotNull RMNIOFileSystem rmNioFileSystem + ) { + super(null, rmNioFileSystem); + this.rmNioFileSystem = rmNioFileSystem; + this.rmProjectId = rmNioFileSystem.getRmProjectId(); + } + + public RMPath( + @NotNull RMNIOFileSystem rmNioFileSystem, + @NotNull String path + ) { + super(path, rmNioFileSystem); + this.rmNioFileSystem = rmNioFileSystem; + this.rmProjectId = rmNioFileSystem.getRmProjectId(); + } + + @Override + public RMNIOFileSystem getFileSystem() { + return rmNioFileSystem; + } + + @Override + public Path getRoot() { + if (isProjectPath()) { + return null; + } + return new RMPath(rmNioFileSystem); + } + + @Override + public Path getFileName() { + return this; + } + + @Override + public Path getParent() { + // project is root and have no parent + if (isProjectPath()) { + return null; + } + + String[] parts = pathParts(); + if (parts.length == 1) { + return new RMPath(rmNioFileSystem); // project is parent + } + //return parent folder + String[] parentParts = Arrays.copyOfRange(parts, 0, parts.length - 1); + return new RMPath(rmNioFileSystem, String.join(getFileSystem().getSeparator(), parentParts)); + } + + @Override + public int getNameCount() { + return pathParts().length; + } + + @Override + public boolean startsWith(@NotNull Path other) { + if (!(other instanceof RMPath)) { + return false; + } + return toString().startsWith(other.toString()); + } + + @Override + public Path normalize() { + return this; + } + + @Override + public Path resolve(Path other) { + RMPath rmOther = (RMPath) other; + if (!rmOther.rmProjectId.equals(rmProjectId)) { + throw new IllegalArgumentException("Cannot resolve path from other project"); + } + return resolve(rmOther.getResourcePath()); + } + + @Override + public Path resolve(String other) { + if (CommonUtils.isEmpty(other)) { + return this; + } + return new RMPath( + rmNioFileSystem, + resolveString(other) + ); + } + + @Override + public URI toUri() { + var fileSystem = getFileSystem(); + var uriBuilder = new StringBuilder(fileSystem.provider().getScheme()) + .append("://") + .append(rmProjectId); + + String rmResourcePath = getResourcePath(); + if (rmResourcePath != null) { + uriBuilder.append(fileSystem.getSeparator()) + .append(rmResourcePath); + } + + return URI.create(uriBuilder.toString()); + } + + @Override + public Path toAbsolutePath() { + if (isAbsolute()) { + return this; + } + return new RMPath(rmNioFileSystem, "/" + path); + } + + @Override + public Path toRealPath(@NotNull LinkOption... options) throws IOException { + return toAbsolutePath(); + } + + @Override + public Path relativize(@NotNull Path other) { + var relativeUri = toUri().resolve(other.toUri()); + return new RMPath(rmNioFileSystem, relativeUri.getPath()); + } + + @Override + public Path getName(int index) { + String[] parts = pathParts(); + if (index < 0 || index > parts.length) { + throw new IllegalArgumentException("Invalid index value: " + index); + } + return new RMPath(rmNioFileSystem, parts[index]); + } + + @Override + public Path subpath(int beginIndex, int endIndex) { + String[] parts = pathParts(); + if (beginIndex < 0 || beginIndex > parts.length) { + throw new IllegalArgumentException("Invalid begin index value: " + beginIndex); + } + + if (endIndex < 0 || endIndex > parts.length || endIndex < beginIndex) { + throw new IllegalArgumentException("Invalid end index value: " + endIndex); + } + + String[] subParts = Arrays.copyOfRange(parts, beginIndex, endIndex); + + return new RMPath(rmNioFileSystem, String.join(getFileSystem().getSeparator(), subParts)); + } + + @Nullable + public String getResourcePath() { + return path; + } + + public boolean isRmRootPath() { + return rmProjectId == null && CommonUtils.isEmpty(path); + } + public boolean isProjectPath() { + return rmProjectId != null && CommonUtils.isEmpty(path); + } + + @NotNull + public String getRmProjectId() throws DBException { + if (rmProjectId == null) { + throw new DBException("Project id not specified"); + } + return rmProjectId; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMResourceBasicAttribute.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMResourceBasicAttribute.java new file mode 100644 index 0000000000..2387e3e07e --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMResourceBasicAttribute.java @@ -0,0 +1,48 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.rm.fs.nio; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.nio.NIOFileBasicAttribute; +import org.jkiss.dbeaver.model.rm.RMResource; + +import java.nio.file.attribute.FileTime; + +public class RMResourceBasicAttribute extends NIOFileBasicAttribute { + + @NotNull + private final RMResource rmResource; + + public RMResourceBasicAttribute(@NotNull RMResource rmResource) { + this.rmResource = rmResource; + } + + @Override + public FileTime lastModifiedTime() { + return FileTime.fromMillis(rmResource.getLastModified()); + } + + @Override + public boolean isDirectory() { + return rmResource.isFolder(); + } + + @Override + public long size() { + return rmResource.getLength(); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMRootBasicAttribute.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMRootBasicAttribute.java new file mode 100644 index 0000000000..5ce279ae3f --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMRootBasicAttribute.java @@ -0,0 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.rm.fs.nio; + +import org.jkiss.dbeaver.model.nio.NIOFileBasicAttribute; + +import java.nio.file.attribute.FileTime; + +public class RMRootBasicAttribute extends NIOFileBasicAttribute { + @Override + public FileTime lastModifiedTime() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isDirectory() { + return true; + } + + @Override + public long size() { + return 0; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index dacdc3ea4d..34d11920e8 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -730,6 +730,12 @@ public RMResource[] getResourcePath(@NotNull String projectId, @NotNull String r return makeResourcePath(projectId, getTargetPath(projectId, resourcePath), false).toArray(RMResource[]::new); } + @Nullable + @Override + public RMResource getResource(@NotNull String projectId, @NotNull String resourcePath) throws DBException { + return makeResourceFromPath(projectId, getTargetPath(projectId, resourcePath), null, false, false, false); + } + @NotNull @Override public byte[] getResourceContents(@NotNull String projectId, @NotNull String resourcePath) throws DBException { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java index fd3c72e29d..a7451a897b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java @@ -23,7 +23,7 @@ public class SessionStateJob extends PeriodicSystemJob { private static final Log log = Log.getLog(SessionStateJob.class); - private static final int PERIOD_MS = 30_000; // once per 60 seconds + private static final int PERIOD_MS = 30_000; // once per 30 seconds public SessionStateJob(@NotNull CBPlatform platform) { super("Session state sender", platform, PERIOD_MS); diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/RMNIOTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/RMNIOTest.java new file mode 100644 index 0000000000..7aeb0c9f6d --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/RMNIOTest.java @@ -0,0 +1,177 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 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.rm; + +import io.cloudbeaver.model.rm.fs.nio.RMNIOFileSystem; +import io.cloudbeaver.model.rm.fs.nio.RMNIOFileSystemProvider; +import io.cloudbeaver.model.rm.fs.nio.RMPath; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.CBConstants; +import io.cloudbeaver.test.platform.CEServerTestSuite; +import io.cloudbeaver.utils.WebTestUtils; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.auth.SMAuthStatus; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.utils.SecurityUtils; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.net.CookieManager; +import java.net.URI; +import java.net.http.HttpClient; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class RMNIOTest { + + private static WebSession webSession; + private static RMProject testProject; + private static RMNIOFileSystemProvider rmFsProvider; + + @BeforeClass + public static void init() throws Exception { + var cookieManager = new CookieManager(); + var client = HttpClient.newBuilder() + .cookieHandler(cookieManager) + .version(HttpClient.Version.HTTP_2) + .build(); + Map authInfo = WebTestUtils.authenticateUser( + client, CEServerTestSuite.getScriptsPath(), CEServerTestSuite.GQL_API_URL); + Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus")); + + String sessionId = cookieManager.getCookieStore().getCookies() + .stream() + .filter(cookie -> cookie.getName().equals(CBConstants.CB_SESSION_COOKIE_NAME)) + .findFirst() + .get() + .getValue(); + webSession = (WebSession) CEServerTestSuite.getTestApp().getSessionManager().getSession(sessionId); + Assert.assertNotNull(webSession); + var projectName = "NIO_Test" + SecurityUtils.generateUniqueId(); + testProject = webSession.getRmController().createProject(projectName, null); + rmFsProvider = new RMNIOFileSystemProvider(webSession.getRmController()); + } + + @AfterClass + public static void destroy() throws Exception { + if (webSession != null && testProject != null) { + webSession.getUserContext().getRmController().deleteProject(testProject.getId()); + } + } + + @Test + public void projectPathTest() throws DBException { + var projectUri = getProjectUri(); + RMPath path = (RMPath) rmFsProvider.getPath(projectUri); + Assert.assertEquals(path.getRmProjectId(), testProject.getId()); + Assert.assertTrue(path.isProjectPath()); + Assert.assertTrue(path.isAbsolute()); + Assert.assertNull(path.getParent()); + Assert.assertNull(path.getRoot()); + Assert.assertTrue(Files.isDirectory(path)); + Assert.assertTrue(Files.exists(path)); + } + + @Test + public void testNotExistProject() { + RMPath notExistPath = new RMPath(new RMNIOFileSystem("s_not_exist", webSession.getRmController(), rmFsProvider)); + Assert.assertFalse(Files.exists(notExistPath)); + } + + @Test + public void testNioProjectOperations() throws IOException, DBException { + var randomName = SecurityUtils.generateUniqueId(); + String randomProject = "s_random_project_" + randomName; + + RMPath newProjectPath = new RMPath(new RMNIOFileSystem(randomProject, webSession.getRmController(), rmFsProvider)); + Assert.assertFalse(Files.exists(newProjectPath)); + + //create project via nio + Files.createDirectory(newProjectPath); + Assert.assertTrue(Files.exists(newProjectPath)); + Assert.assertNotNull(webSession.getRmController().getProject(randomProject, false, false)); + + //delete project via nio + Files.delete(newProjectPath); + Assert.assertFalse(Files.exists(newProjectPath)); + Assert.assertNull(webSession.getRmController().getProject(randomProject, false, false)); + } + + @Test + public void testResolve() { + RMPath rootPath = (RMPath) rmFsProvider.getPath(getProjectUri()); + String scriptName = "child.sql"; + Path scriptPath = rootPath.resolve(scriptName); + Assert.assertEquals(getProjectUri() + "/" + "child.sql", scriptPath.toString()); + } + + @Test + public void testListFiles() throws DBException, IOException { + RMPath rootPath = (RMPath) rmFsProvider.getPath(getProjectUri()); + RMController rm = webSession.getRmController(); + String file1 = "folder" + SecurityUtils.generateUniqueId(); + String file2 = "script" + SecurityUtils.generateUniqueId() + ".sql"; + rm.createResource(testProject.getId(), file1, true); + rm.createResource(testProject.getId(), file2, false); + Set filesFromNio = + Files.list(rootPath) + .map(path -> ((RMPath) path).getResourcePath()) + .collect(Collectors.toSet()); + Assert.assertTrue(filesFromNio.contains(file1)); + Assert.assertTrue(filesFromNio.contains(file2)); + } + + @Test + public void testWriteResource() throws IOException, DBException { + RMPath rootPath = (RMPath) rmFsProvider.getPath(getProjectUri()); + RMController rm = webSession.getRmController(); + String script = "test_script_" + SecurityUtils.generateUniqueId() + ".sql"; + RMPath scriptPath = (RMPath) rootPath.resolve(script); + + //create file + Files.createFile(scriptPath); + Assert.assertTrue(Files.exists(scriptPath)); + Assert.assertNotNull(rm.getResource(testProject.getId(), script)); + + //set content rm://s_test_project/test_script.sql + + String sql = "select " + SecurityUtils.getRandom().nextInt(1000); + Files.writeString(scriptPath, sql); + String dataFromNio = Files.readString(scriptPath); + String dataFromRM = new String(rm.getResourceContents(testProject.getId(), script)); + Assert.assertEquals(sql, dataFromNio); + Assert.assertEquals(sql, dataFromRM); + + //delete + Files.delete(scriptPath); + Assert.assertFalse(Files.exists(scriptPath)); + Assert.assertNull(rm.getResource(testProject.getId(), script)); + } + + + private URI getProjectUri() { + return URI.create("rm://" + testProject.getId()); + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java index cea79cfe56..9bd50a7fd8 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java @@ -17,6 +17,7 @@ package io.cloudbeaver.test.platform; +import io.cloudbeaver.model.rm.RMNIOTest; import io.cloudbeaver.model.rm.lock.RMLockTest; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBApplicationCE; @@ -36,7 +37,8 @@ ConnectionsTest.class, AuthenticationTest.class, ResourceManagerTest.class, - RMLockTest.class + RMLockTest.class, + RMNIOTest.class } ) public class CEServerTestSuite { From 571d6b7c0a6af1a250f4b6ed01b23b038530a437 Mon Sep 17 00:00:00 2001 From: alex <48489896+devnaumov@users.noreply.github.com> Date: Thu, 28 Sep 2023 01:02:49 +0200 Subject: [PATCH 2/2] CB-3864 migrate tree to css modules (#2003) * CB-3864 migrate tree to css modules * CB-3864 migrate extra components * CB-3864 review fixes * CB-3864 move editing and dragging props from base nav components * CB-3864 add styles to renderer * CB-3864 fix aws styles * CB-3864 remove padding in projects renderer * CB-3864 fix node hover effect * CB-3864 remove custom tags * CB-3864 remove config changes * CB-3864 review fixes * CB-3864 move NavigationNodeControl styles closer to the component * CB-3864 chore: code cleanup --------- Co-authored-by: Alexey Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com> Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com> --- .../core-blocks/src/ActionIconButton.m.css | 10 + .../ConnectionMark.m.css | 16 ++ .../ConnectionMark.tsx | 30 +-- .../packages/core-blocks/src/IconButton.m.css | 2 +- .../src/Tree/TreeNode/TreeNode.m.css | 5 + .../src/Tree/TreeNode/TreeNode.tsx | 15 +- .../src/Tree/TreeNode/TreeNodeControl.m.css | 24 +++ .../src/Tree/TreeNode/TreeNodeControl.tsx | 9 +- .../src/Tree/TreeNode/TreeNodeExpand.m.css | 23 +++ .../src/Tree/TreeNode/TreeNodeExpand.tsx | 25 ++- .../src/Tree/TreeNode/TreeNodeIcon.m.css | 12 ++ .../src/Tree/TreeNode/TreeNodeIcon.tsx | 29 ++- .../src/Tree/TreeNode/TreeNodeName.m.css | 8 + .../src/Tree/TreeNode/TreeNodeName.tsx | 13 +- .../src/Tree/TreeNode/TreeNodeNested.m.css | 10 + .../src/Tree/TreeNode/TreeNodeNested.tsx | 23 ++- .../Tree/TreeNode/TreeNodeNestedMessage.m.css | 4 + .../Tree/TreeNode/TreeNodeNestedMessage.tsx | 13 +- .../src/Tree/TreeNode/TreeNodeSelect.m.css | 4 + .../src/Tree/TreeNode/TreeNodeSelect.tsx | 22 +- .../src/Tree/TreeNode/TreeNodeStyles.ts | 188 ------------------ webapp/packages/core-blocks/src/index.ts | 4 +- .../core-cli/configs/webpack.config.js | 1 + .../ErrorDetailsDialog/ErrorDetailsDialog.tsx | 10 +- .../FiltersTableItem.tsx | 11 +- .../NavigationNodeProjectControl.m.css | 35 ++++ .../NavigationNodeProjectControl.tsx | 58 +----- ...vigationTreeProjectsRendererRenderer.m.css | 12 ++ ...navigationTreeProjectsRendererRenderer.tsx | 73 +++---- .../src/Tree/ResourceManagerTree.m.css | 29 +++ .../src/Tree/ResourceManagerTree.tsx | 58 ++---- ...avigationTreeConnectionGroupRenderer.m.css | 8 + .../navigationTreeConnectionGroupRenderer.tsx | 30 +-- .../ConnectionNavNodeControl.tsx | 31 +-- .../navTreeConnectionRenderer.tsx | 7 +- .../ElementsTree/ElementsTree.m.css | 51 +++++ .../ElementsTree/ElementsTree.tsx | 96 ++------- .../ElementsTreeContentLoader.tsx | 7 +- .../ElementsTreeFilter.m.css | 5 + .../ElementsTreeTools/ElementsTreeFilter.tsx | 20 +- .../ElementsTreeTools/ElementsTreeTools.tsx | 18 +- .../ElementsTree/NavigationNodeComponent.ts | 1 + .../NavigationTreeNode/NavigationNode.m.css | 9 + .../NavigationTreeNode/NavigationNode.tsx | 30 +-- .../NAVIGATION_NODE_CONTROL_STYLES.ts | 39 ---- .../NavigationNodeControl.m.css | 41 ++++ .../NavigationNode/NavigationNodeControl.tsx | 25 +-- .../NavigationNode/NavigationNodeEditor.m.css | 9 + .../NavigationNode/NavigationNodeEditor.tsx | 21 +- .../NavigationNode/NavigationNodeExpand.tsx | 5 +- .../NavigationNode/NavigationNodeNested.m.css | 3 + .../NavigationNode/NavigationNodeNested.tsx | 13 +- .../NavigationNodeControlRenderer.m.css | 8 + .../NavigationNodeControlRenderer.tsx | 31 ++- .../NavigationNodeDragged.tsx | 9 +- .../NavigationNodeElement.tsx | 14 +- .../TreeNodeMenu/TreeNodeMenu.m.css | 21 ++ .../TreeNodeMenu/TreeNodeMenu.tsx | 20 +- .../NavigationTree/ElementsTreeTools.m.css | 8 + .../src/NavigationTree/NavigationTree.m.css | 29 +++ .../src/NavigationTree/NavigationTree.tsx | 113 ++++------- .../NavigationTree/NavigationTreePanel.m.css | 6 + .../NavigationTree/NavigationTreePanel.tsx | 20 +- .../NavigationNodeProjectControl.m.css | 35 ++++ .../NavigationNodeProjectControl.tsx | 57 +----- ...vigationTreeProjectsRendererRenderer.m.css | 12 ++ ...navigationTreeProjectsRendererRenderer.tsx | 70 +++---- .../plugin-navigation-tree/src/index.ts | 4 + 68 files changed, 841 insertions(+), 861 deletions(-) create mode 100644 webapp/packages/core-blocks/src/ActionIconButton.m.css create mode 100644 webapp/packages/core-blocks/src/ConnectionImageWithMask/ConnectionMark.m.css create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.m.css create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.m.css create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.m.css create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.m.css create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeName.m.css create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.m.css create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNestedMessage.m.css create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.m.css delete mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts create mode 100644 webapp/packages/plugin-navigation-tree-rm/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.m.css create mode 100644 webapp/packages/plugin-navigation-tree-rm/src/Tree/ProjectsRenderer/NavigationTreeProjectsRendererRenderer.m.css create mode 100644 webapp/packages/plugin-navigation-tree-rm/src/Tree/ResourceManagerTree.m.css create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ConnectionGroup/NavigationTreeConnectionGroupRenderer.m.css create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeFilter.m.css create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.m.css delete mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NAVIGATION_NODE_CONTROL_STYLES.ts create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.m.css create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.m.css create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.m.css create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNodeControlRenderer.m.css create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.m.css create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTreeTools.m.css create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.m.css create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTreePanel.m.css create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.m.css create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationTreeProjectsRendererRenderer.m.css diff --git a/webapp/packages/core-blocks/src/ActionIconButton.m.css b/webapp/packages/core-blocks/src/ActionIconButton.m.css new file mode 100644 index 0000000000..62d354bcc9 --- /dev/null +++ b/webapp/packages/core-blocks/src/ActionIconButton.m.css @@ -0,0 +1,10 @@ +.actionIconButton { + composes: theme-form-element-radius theme-ripple from global; + + padding: 4px !important; + margin: 2px !important; + width: 24px !important; + height: 24px !important; + overflow: hidden; + flex-shrink: 0; +} diff --git a/webapp/packages/core-blocks/src/ConnectionImageWithMask/ConnectionMark.m.css b/webapp/packages/core-blocks/src/ConnectionImageWithMask/ConnectionMark.m.css new file mode 100644 index 0000000000..8f1533dd3a --- /dev/null +++ b/webapp/packages/core-blocks/src/ConnectionImageWithMask/ConnectionMark.m.css @@ -0,0 +1,16 @@ +.status { + composes: theme-background-positive from global; + position: absolute; + opacity: 0; + transition: opacity 0.3s ease; + bottom: 0; + right: 0; + box-sizing: border-box; + width: 8px; + height: 8px; + border-radius: 50%; + + &.connected { + opacity: 1; + } +} diff --git a/webapp/packages/core-blocks/src/ConnectionImageWithMask/ConnectionMark.tsx b/webapp/packages/core-blocks/src/ConnectionImageWithMask/ConnectionMark.tsx index 1f9ebb35a8..22d0a349ee 100644 --- a/webapp/packages/core-blocks/src/ConnectionImageWithMask/ConnectionMark.tsx +++ b/webapp/packages/core-blocks/src/ConnectionImageWithMask/ConnectionMark.tsx @@ -5,32 +5,18 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import styled, { css, use } from 'reshadow'; +import { observer } from 'mobx-react-lite'; -const styles = css` - status { - composes: theme-background-positive from global; - position: absolute; - opacity: 0; - transition: opacity 0.3s ease; - bottom: 0; - right: 0; - box-sizing: border-box; - width: 8px; - height: 8px; - border-radius: 50%; - - &[|connected] { - opacity: 1; - } - } -`; +import { s } from '../s'; +import { useS } from '../useS'; +import style from './ConnectionMark.m.css'; interface Props { connected: boolean; className?: string; } -export const ConnectionMark: React.FC = function ConnectionMark({ connected, className }) { - return styled(styles)(); -}; +export const ConnectionMark: React.FC = observer(function ConnectionMark({ connected, className }) { + const styles = useS(style); + return
; +}); diff --git a/webapp/packages/core-blocks/src/IconButton.m.css b/webapp/packages/core-blocks/src/IconButton.m.css index 61fd320c09..aab16ad798 100644 --- a/webapp/packages/core-blocks/src/IconButton.m.css +++ b/webapp/packages/core-blocks/src/IconButton.m.css @@ -14,4 +14,4 @@ width: 100%; height: 100%; } -} \ No newline at end of file +} diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.m.css b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.m.css new file mode 100644 index 0000000000..8949559b9a --- /dev/null +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.m.css @@ -0,0 +1,5 @@ +.node { + box-sizing: border-box; + width: inherit; + min-width: 100%; +} diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx index 256c38d27f..cc16d0f789 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx @@ -8,14 +8,14 @@ import { computed, observable } from 'mobx'; import { observer } from 'mobx-react-lite'; import { forwardRef } from 'react'; -import styled, { use } from 'reshadow'; -import { getComputed } from '../../getComputed'; +import { s } from '../../s'; import { useObjectRef } from '../../useObjectRef'; import { useObservableRef } from '../../useObservableRef'; +import { useS } from '../../useS'; import type { ITreeNodeState } from './ITreeNodeState'; +import style from './TreeNode.m.css'; import { ITreeNodeContext, TreeNodeContext } from './TreeNodeContext'; -import { TREE_NODE_STYLES } from './TreeNodeStyles'; interface Props extends ITreeNodeState { className?: string; @@ -48,6 +48,7 @@ export const TreeNode = observer( }, ref, ) { + const styles = useS(style); const handlersRef = useObjectRef(handlers); async function processAction(action: () => Promise) { @@ -113,12 +114,10 @@ export const TreeNode = observer( }, ); - const elementExpanded = getComputed(() => nodeContext.externalExpanded ?? nodeContext.expanded); - - return styled(TREE_NODE_STYLES)( - + return ( +
{children} - , +
); }), ); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.m.css b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.m.css new file mode 100644 index 0000000000..9147a3540b --- /dev/null +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.m.css @@ -0,0 +1,24 @@ +.treeNodeControl { + composes: theme-ripple theme-ripple-selectable from global; + box-sizing: border-box; + height: 22px; + display: flex; + align-items: center; + padding: 2px 0 2px 0; + user-select: none; + white-space: nowrap; + position: initial !important; + outline: none; + + &::before { + left: 0 !important; + top: auto !important; + height: inherit !important; + width: 100% !important; + } + + & > * { + margin-right: 0; + margin-left: 4px; + } +} diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx index cfd07665b3..f0a6f97052 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx @@ -10,11 +10,14 @@ import React, { forwardRef, useContext } from 'react'; import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; +import { s } from '../../s'; +import { useS } from '../../useS'; import { EventTreeNodeClickFlag } from './EventTreeNodeClickFlag'; import { EventTreeNodeExpandFlag } from './EventTreeNodeExpandFlag'; import { EventTreeNodeSelectFlag } from './EventTreeNodeSelectFlag'; import type { ITreeNodeState } from './ITreeNodeState'; import { TreeNodeContext } from './TreeNodeContext'; +import style from './TreeNodeControl.m.css'; const KEY = { ENTER: 'Enter', @@ -25,15 +28,15 @@ interface Props extends ITreeNodeState { onClick?: (event: React.MouseEvent) => void; onMouseDown?: (event: React.MouseEvent) => void; className?: string; - big?: boolean; children?: React.ReactNode; } export const TreeNodeControl = observer, HTMLDivElement>( forwardRef(function TreeNodeControl( - { title, group, disabled, loading, selected, expanded, externalExpanded, leaf, onClick, onMouseDown, className, children, big, ...rest }, + { title, group, disabled, loading, selected, expanded, externalExpanded, leaf, onClick, onMouseDown, className, children, ...rest }, ref, ) { + const styles = useS(style); const context = useContext(TreeNodeContext); if (!context) { @@ -112,7 +115,7 @@ export const TreeNodeControl = observer(function TreeNodeExpand({ leaf, big, filterActive, disabled, className }) { + const styles = useS(style); const context = useContext(TreeNodeContext); if (!context) { @@ -90,10 +85,14 @@ export const TreeNodeExpand = observer(function TreeNodeExpand({ leaf, bi } } - return styled(styles)( - + return ( +
{loading && } - {expandable && } - , + {expandable && } +
); }); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.m.css b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.m.css new file mode 100644 index 0000000000..20eb274d11 --- /dev/null +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.m.css @@ -0,0 +1,12 @@ +.treeNodeIcon { + position: relative; + box-sizing: border-box; + flex-shrink: 0; + width: 16px; + height: 16px; +} + +.staticImage { + position: relative; + height: 100%; +} diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.tsx index a7841dc780..ea596bd1c5 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.tsx @@ -5,30 +5,25 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import styled, { css } from 'reshadow'; - -import type { ComponentStyle } from '@cloudbeaver/core-theming'; +import { observer } from 'mobx-react-lite'; +import { s } from '../../s'; import { StaticImage } from '../../StaticImage'; -import { useStyles } from '../../useStyles'; - -const styles = css` - StaticImage { - height: 100%; - } -`; +import { useS } from '../../useS'; +import style from './TreeNodeIcon.m.css'; interface Props { icon?: string; - style?: ComponentStyle; className?: string; } -export const TreeNodeIcon: React.FC> = function TreeNodeIcon({ icon, style, className, children }) { - return styled(useStyles(styles, style))( - - {icon && } +export const TreeNodeIcon: React.FC> = observer(function TreeNodeIcon({ icon, className, children }) { + const styles = useS(style); + + return ( +
+ {icon && } {children} - , +
); -}; +}); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeName.m.css b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeName.m.css new file mode 100644 index 0000000000..cb76e298e2 --- /dev/null +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeName.m.css @@ -0,0 +1,8 @@ +.treeNodeName { + position: relative; + box-sizing: border-box; + padding-right: 0; + display: flex; + align-items: center; + margin-right: 4px; +} diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeName.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeName.tsx index 891ef816e0..597bfe10f5 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeName.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeName.tsx @@ -5,15 +5,22 @@ * 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 { s } from '../../s'; +import { useS } from '../../useS'; +import style from './TreeNodeName.m.css'; interface Props extends React.HTMLAttributes { className?: string; } -export const TreeNodeName: React.FC = function TreeNodeName({ className, children, ...rest }) { +export const TreeNodeName: React.FC = observer(function TreeNodeName({ className, children, ...rest }) { + const styles = useS(style); + return ( -
+
{children}
); -}; +}); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.m.css b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.m.css new file mode 100644 index 0000000000..cbb45c7cba --- /dev/null +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.m.css @@ -0,0 +1,10 @@ +.treeNodeNested { + box-sizing: border-box; + padding-left: 8px; + display: none; + + &.root { + padding: 0; + display: block; + } +} diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.tsx index 8c15519db5..b8281fbe8c 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.tsx @@ -5,18 +5,27 @@ * 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 { forwardRef } from 'react'; +import { s } from '../../s'; +import { useS } from '../../useS'; +import style from './TreeNodeNested.m.css'; + interface Props extends React.PropsWithChildren { expanded?: boolean; root?: boolean; className?: string; } -export const TreeNodeNested = forwardRef(function TreeNodeNested({ className, children }, ref) { - return ( -
- {children} -
- ); -}); +export const TreeNodeNested = observer( + forwardRef(function TreeNodeNested({ root, className, children }, ref) { + const styles = useS(style); + + return ( +
+ {children} +
+ ); + }), +); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNestedMessage.m.css b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNestedMessage.m.css new file mode 100644 index 0000000000..d1e5ec397d --- /dev/null +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNestedMessage.m.css @@ -0,0 +1,4 @@ +.treeNodeNestedMessage { + composes: theme-typography--caption from global; + padding: 4px 12px; +} diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNestedMessage.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNestedMessage.tsx index 63bfe78446..bc7a8bb4b7 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNestedMessage.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNestedMessage.tsx @@ -5,12 +5,17 @@ * 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 { s } from '../../s'; +import { useS } from '../../useS'; +import style from './TreeNodeNestedMessage.m.css'; interface Props { - big?: boolean; className?: string; } -export const TreeNodeNestedMessage: React.FC> = function TreeNodeNestedMessage({ className, children }) { - return
{children}
; -}; +export const TreeNodeNestedMessage: React.FC> = observer(function TreeNodeNestedMessage({ className, children }) { + const styles = useS(style); + return
{children}
; +}); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.m.css b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.m.css new file mode 100644 index 0000000000..88cba18609 --- /dev/null +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.m.css @@ -0,0 +1,4 @@ +.loader { + width: 40px; + height: 40px; +} diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx index 93f0b39737..e84b5ad014 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx @@ -7,21 +7,16 @@ */ import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; -import styled, { css } from 'reshadow'; import { EventContext } from '@cloudbeaver/core-events'; import { Checkbox } from '../../FormControls/Checkboxes/Checkbox'; import { Loader } from '../../Loader/Loader'; +import { s } from '../../s'; +import { useS } from '../../useS'; import { EventTreeNodeSelectFlag } from './EventTreeNodeSelectFlag'; import { TreeNodeContext } from './TreeNodeContext'; - -const styles = css` - Loader { - width: 40px; - height: 40px; - } -`; +import style from './TreeNodeSelect.m.css'; interface Props { group?: boolean; @@ -42,6 +37,7 @@ export const TreeNodeSelect = observer(function TreeNodeSelect({ loadIndicator, className, }) { + const styles = useS(style); const context = useContext(TreeNodeContext); if (!context) { @@ -67,9 +63,13 @@ export const TreeNodeSelect = observer(function TreeNodeSelect({ EventContext.set(event, EventTreeNodeSelectFlag); } - return styled(styles)( + return (
- {loading ? : } -
, + {loading ? ( + + ) : ( + + )} +
); }); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts deleted file mode 100644 index 3dc933e13c..0000000000 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts +++ /dev/null @@ -1,188 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 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 { css } from 'reshadow'; - -export const TREE_NODE_STYLES = css` - TreeNodeControl { - composes: theme-ripple theme-ripple-selectable from global; - } - TreeNodeNestedMessage { - composes: theme-text-text-hint-on-light from global; - } - node { - box-sizing: border-box; - width: inherit; - min-width: 100%; - - &[use|expanded] { - & > TreeNodeNested { - display: block; - } - - & > TreeNodeControl > TreeNodeExpand { - transform: rotate(0deg); - } - } - - & TreeNodeControl { - box-sizing: border-box; - height: 22px; - display: flex; - align-items: center; - padding: 2px 0 2px 0; - user-select: none; - white-space: nowrap; - position: initial; - outline: none; - - &::before { - left: 0; - top: auto; - height: inherit; - width: 100%; - } - - &[|dragging] { - opacity: 0.6; - } - - &[|editing]::before { - display: none; - } - - & > * { - margin-right: 0; - margin-left: 4px; - } - - & TreeNodeName { - margin-right: 4px; - } - } - } - - TreeNodeExpand { - display: flex; - box-sizing: border-box; - flex-shrink: 0; - opacity: 0.5; - width: 16px; - height: 16px; - transform: rotate(-90deg); - & [|size='small'] { - display: block; - } - - & [|size='big'] { - display: none; - } - } - - NavigationNode[dragging] { - opacity: 0.5 !important; - } - - TreeNodeIcon { - box-sizing: border-box; - flex-shrink: 0; - width: 16px; - height: 16px; - } - - TreeNodeName { - box-sizing: border-box; - padding-right: 0; - display: flex; - align-items: center; - margin-right: 4px; - } - - TreeNodeFilter { - position: relative; - min-width: 24px; - min-height: 24px; - display: flex; - align-items: center; - } - - TreeNodeIcon, - TreeNodeExpand, - TreeNodeName { - position: relative; - } - - TreeNodeNested { - box-sizing: border-box; - padding-left: 8px; - display: none; - - &[root] { - padding: 0; - display: block; - } - - &[expanded] { - display: block; - } - } - - TreeNodeNestedMessage { - composes: theme-typography--caption from global; - padding: 4px 12px; - - &[big] { - padding: 4px 24px; - } - } - - tree-box[|big] { - & TreeNodeNestedMessage { - padding: 4px 24px; - } - } - - node TreeNodeControl[big] { - height: 46px; - padding: 0px 16px; - - & TreeNodeName { - margin: 0; - padding-right: 8px; - padding-left: 8px; - } - & TreeNodeIcon { - width: 24px; - height: 24px; - } - & TreeNodeExpand { - width: 16px; - height: 16px; - } - - & TreeNodeExpand [|size='small'] { - display: none; - } - - & TreeNodeExpand [|size='big'] { - display: block; - } - - & > * { - margin-right: 8px; - margin-left: 8px; - } - - & TreeNodeSelect { - margin: 0; - } - } - - node TreeNodeControl[big] + TreeNodeNested { - padding-left: 24px; - } -`; diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index a9d36511d4..d0fcbea067 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -25,6 +25,7 @@ export * from './ConnectionImageWithMask/ConnectionImageWithMask'; export { default as ConnectionImageWithMaskSvgStyles } from './ConnectionImageWithMask/ConnectionImageWithMaskSvg.m.css'; export * from './Menu/ACTION_ICON_BUTTON_STYLES'; +export { default as ActionIconButtonStyles } from './ActionIconButton.m.css'; export * from './Menu/Menu'; export { default as MenuStyles } from './Menu/Menu.m.css'; export * from './Menu/MenuBarSmallItem'; @@ -106,12 +107,13 @@ export * from './Tree/TreeNode/TreeNodeName'; export * from './Tree/TreeNode/TreeNodeNested'; export * from './Tree/TreeNode/TreeNodeNestedMessage'; export * from './Tree/TreeNode/TreeNodeSelect'; -export * from './Tree/TreeNode/TreeNodeStyles'; export * from './Button'; export * from './ToolsPanel/ToolsAction'; export * from './ToolsPanel/ToolsPanel'; export { default as ToolsPanelStyles } from './ToolsPanel/ToolsPanel.m.css'; export { default as ToolsActionStyles } from './ToolsPanel/ToolsAction.m.css'; +export { default as TreeNodeNestedMessageStyles } from './Tree/TreeNode/TreeNodeNestedMessage.m.css'; +export { default as TreeNodeStyles } from './Tree/TreeNode/TreeNode.m.css'; export * from './FormControls/Checkboxes/Checkbox'; export * from './FormControls/Checkboxes/FieldCheckbox'; export * from './FormControls/Checkboxes/CheckboxMarkup'; diff --git a/webapp/packages/core-cli/configs/webpack.config.js b/webapp/packages/core-cli/configs/webpack.config.js index d43cd1c56a..8976486b15 100644 --- a/webapp/packages/core-cli/configs/webpack.config.js +++ b/webapp/packages/core-cli/configs/webpack.config.js @@ -242,6 +242,7 @@ module.exports = (env, argv) => { plugins: [ new ForkTsCheckerWebpackPlugin({ typescript: { + typescriptPath: require.resolve('typescript'), configFile: resolve('tsconfig.json'), configOverwrite: { include: ['**/src/**/*.ts', '**/src/**/*.tsx'], diff --git a/webapp/packages/core-notifications/src/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx b/webapp/packages/core-notifications/src/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx index e1ac728334..35bd0e3044 100644 --- a/webapp/packages/core-notifications/src/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx +++ b/webapp/packages/core-notifications/src/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx @@ -19,13 +19,13 @@ function DisplayErrorInfo({ error }: { error: IErrorInfo }) { return ( <> - - {error.isHtml ?