Merge branch 'devel' into CB-4356-create-smart-tabs-switchingng-for-forms

This commit is contained in:
s.teleshev
2023-12-13 09:07:17 +01:00
44 changed files with 433 additions and 536 deletions
@@ -7,6 +7,7 @@
*/
import { css } from 'reshadow';
// TODO in cloudeaver-ee remove all reshadow styles connected to this
export const ADMINISTRATION_TOOLS_PANEL_STYLES = css`
ToolsPanel {
composes: theme-background-surface theme-text-on-surface theme-border-color-background from global;
@@ -0,0 +1,30 @@
.message {
box-sizing: border-box;
display: flex;
align-items: center;
border-radius: var(--theme-group-element-radius);
height: 50px;
padding: 8px 12px;
}
.errorIcon {
width: 24px;
height: 24px;
margin-right: 8px;
}
.messageBody {
composes: theme-typography--body2 from global;
max-height: 100%;
line-height: 1.2;
box-sizing: border-box;
flex: 1;
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
.messageActions {
margin-left: 16px;
}
@@ -6,43 +6,13 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { Button } from './Button';
import { IconOrImage } from './IconOrImage';
import { useTranslate } from './localization/useTranslate';
import { useS } from './useS';
const styles = css`
message {
box-sizing: border-box;
display: flex;
align-items: center;
border-radius: var(--theme-group-element-radius);
height: 50px;
padding: 8px 12px;
}
IconOrImage {
width: 24px;
height: 24px;
margin-right: 8px;
}
message-body {
composes: theme-typography--body2 from global;
max-height: 100%;
line-height: 1.2;
box-sizing: border-box;
flex: 1;
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
message-actions {
margin-left: 16px;
}
`;
import styles from './ErrorMessage.m.css';
import { s } from './s';
interface Props {
hasDetails?: boolean;
@@ -53,18 +23,19 @@ interface Props {
export const ErrorMessage = observer<Props>(function ErrorMessage({ text, className, hasDetails, onShowDetails }) {
const translate = useTranslate();
const style = useS(styles);
return styled(styles)(
<message className={className}>
<IconOrImage icon="/icons/error_icon_sm.svg" />
<message-body title={text}>{text}</message-body>
<message-actions>
return (
<div className={s(style, { message: true }, className)}>
<IconOrImage className={s(style, { errorIcon: true })} icon="/icons/error_icon_sm.svg" />
<div className={s(style, { messageBody: true })} title={text}>{text}</div>
<div className={s(style, { messageActions: true })}>
{hasDetails && (
<Button type="button" mod={['outlined']} onClick={onShowDetails}>
{translate('ui_errors_details')}
</Button>
)}
</message-actions>
</message>,
</div>
</div>
);
});
@@ -6,7 +6,6 @@
* you may not use this file except in compliance with the License.
*/
import { useCallback, useRef, useState } from 'react';
import { use } from 'reshadow';
import { IconButton } from '../IconButton';
import { useTranslate } from '../localization/useTranslate';
@@ -67,7 +66,6 @@ export const ItemListSearch: React.FC<IProps> = function ItemListSearch({ value,
autoComplete="off"
disabled={disabled}
onChange={event => changeHandler(event.target.value)}
{...use({ mod: 'surface' })}
/>
<div className={s(styles, { actionButton: true })}>
{!onSearch && inputValue ? (
@@ -1,34 +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 { useEffect, useRef } from 'react';
import styled from 'reshadow';
import { Icon } from '../Icon';
import styles from './loading-screen.css';
const FADE_IN_DELAY = 300;
function LoadingScreen() {
const ref = useRef<HTMLDivElement>(null);
useEffect(
() => () => {
const appContainer = document.querySelector('#__next')!;
const loadingNode = appContainer.appendChild(ref.current!.cloneNode(true));
setTimeout(() => (loadingNode as any).classList.add(styles.fadeIn));
setTimeout(() => appContainer.removeChild(loadingNode), FADE_IN_DELAY);
},
[],
);
return styled(styles)(
<div ref={ref}>
<Icon name="logo" viewBox="0 0 1755.6533 681.13202" />
</div>,
);
}
@@ -1,56 +0,0 @@
:global(html) {
font-family: 'Roboto', sans-serif;
}
div {
display: flex;
position: absolute;
background-color: #ffffff;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.fadeIn {
transition: opacity 0.3s ease;
opacity: 0;
animation: none;
}
Icon {
position: relative;
width: 100%;
margin: auto;
max-width: 600px;
}
div::before {
content: ' ';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 50%;
z-index: 1;
width: 500%;
margin-left: -250%;
animation: wave 1.3s linear infinite;
background: linear-gradient(
to right,
rgba(255, 255, 255, 0) 46%,
rgba(255, 255, 255, 0.35) 50%,
rgba(255, 255, 255, 0) 54%
)
50% 50%;
}
@keyframes wave {
0% {
transform: translate3d(-30%, 0, 0);
}
100% {
transform: translate3d(30%, 0, 0);
}
}
@@ -6,7 +6,6 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { Icon } from '../Icon';
import { s } from '../s';
@@ -0,0 +1,4 @@
.checkbox {
margin-left: -10px;
margin-right: -10px;
}
@@ -7,7 +7,6 @@
*/
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import styled, { css } from 'reshadow';
import { EventContext } from '@cloudbeaver/core-events';
@@ -15,6 +14,9 @@ import { Checkbox } from '../FormControls/Checkboxes/Checkbox';
import { EventTableItemSelectionFlag } from './EventTableItemSelectionFlag';
import { TableContext } from './TableContext';
import { TableItemContext } from './TableItemContext';
import { useS } from '../useS';
import styles from './TableItemSelect.m.css';
import { s } from '../s';
interface Props {
checked?: boolean;
@@ -23,17 +25,11 @@ interface Props {
className?: string;
}
const styles = css`
Checkbox {
margin-left: -10px;
margin-right: -10px;
}
`;
export const TableItemSelect = observer<Props>(function TableItemSelect({ checked, disabled, tooltip, className }) {
const tableContext = useContext(TableContext);
const context = useContext(TableItemContext);
const selected = checked ?? context?.isSelected();
const style = useS(styles);
function handleClick(event: React.MouseEvent<HTMLInputElement>) {
if (!context) {
@@ -48,7 +44,13 @@ export const TableItemSelect = observer<Props>(function TableItemSelect({ checke
return null;
}
return styled(styles)(
<Checkbox className={className} title={tooltip} disabled={context.selectDisabled || disabled} checked={selected} onClick={handleClick} />,
return (
<Checkbox
className={s(style, { checkbox: true }, className)}
title={tooltip}
disabled={context.selectDisabled || disabled}
checked={selected}
onClick={handleClick}
/>
);
});
@@ -1,5 +1,22 @@
.toolsPanel {
height: 30px;
display: flex;
flex-shrink: 0;
flex: 0 0 auto;
}
.primary {
composes: theme-background-surface theme-text-on-surface theme-border-color-background from global;
}
.bottomBorder {
border-bottom: solid 1px;
}
.secondary {
composes: theme-background-secondary theme-text-on-secondary theme-border-color-background from global;
overflow: auto;
align-items: center;
height: initial;
min-height: 32px;
border-bottom: none;
}
@@ -9,12 +9,15 @@ import { s } from '../s';
import { useS } from '../useS';
import style from './ToolsPanel.m.css';
type TType = 'primary' | 'secondary';
interface Props {
className?: string;
type?: TType;
hasBottomBorder?: boolean;
}
export const ToolsPanel: React.FC<React.PropsWithChildren<Props>> = function ToolsPanel({ className, children }) {
export const ToolsPanel: React.FC<React.PropsWithChildren<Props>> = function ToolsPanel({ className, children, type = 'primary', hasBottomBorder = false }) {
const styles = useS(style);
return <div className={s(styles, { toolsPanel: true }, className)}>{children}</div>;
return <div className={s(styles, { toolsPanel: true, [type]: true, bottomBorder: hasBottomBorder }, className)}>{children}</div>;
};
@@ -0,0 +1,11 @@
.container {
height: 100%;
display: flex;
align-items: center;
padding: 0 14px;
cursor: pointer;
}
.logo {
height: 32px;
}
@@ -5,39 +5,12 @@
* 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 { IconOrImage } from '../IconOrImage';
import { s } from '../s';
import { useS } from '../useS';
import styles from './AppLogo.m.css';
const styles = css`
logo {
height: 100%;
display: flex;
align-items: center;
padding: 0 14px;
cursor: pointer;
}
IconOrImage {
height: 32px;
width: 154px;
&[|small] {
display: none;
}
}
@media only screen and (min-width: 480px) {
IconOrImage {
&[|primary] {
display: none;
}
&[|small] {
display: block;
width: auto;
}
}
}
`;
interface Props {
title: string;
@@ -45,10 +18,10 @@ interface Props {
}
export const AppLogo: React.FC<Props> = function AppLogo({ title, onClick }) {
return styled(styles)(
<logo title={title} onClick={onClick}>
<IconOrImage icon="/icons/logo.svg" {...use({ primary: true })} />
<IconOrImage icon="/icons/logo_sm.svg" {...use({ small: true })} />
</logo>,
const style = useS(styles);
return (
<div className={s(style, { container: true })} title={title} onClick={onClick}>
<IconOrImage className={s(style, { logo: true })} icon="/icons/logo_sm.svg" />
</div>
);
};
@@ -0,0 +1,10 @@
.header {
composes: theme-text-on-primary theme-typography--body2 from global;
display: flex;
align-items: center;
height: 48px;
padding: 0 8px;
background-color: #2a7cb4;
z-index: 1;
flex-shrink: 0;
}
@@ -5,25 +5,17 @@
* 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';
export const topAppBarStyles = css`
header {
composes: theme-text-on-primary theme-typography--body2 from global;
display: flex;
align-items: center;
height: 48px;
padding: 0 8px;
background-color: #2a7cb4;
z-index: 1;
flex-shrink: 0;
}
`;
import { s } from '../s';
import { useS } from '../useS';
import styles from './TopAppBar.m.css';
interface Props extends React.PropsWithChildren {
className?: string;
}
export const TopAppBar: React.FC<Props> = function TopAppBar({ children, className }) {
return styled(topAppBarStyles)(<header className={className}>{children}</header>);
const style = useS(styles);
return (<header className={s(style, { header: true }, className)}>{children}</header>);
};
@@ -0,0 +1,3 @@
.loader {
height: 100vh;
}
@@ -7,11 +7,11 @@
*/
import { Suspense } from 'react';
import { createRoot, Root } from 'react-dom/client';
import styled from 'reshadow';
import { Body } from '@cloudbeaver/core-app';
import { DisplayError, ErrorBoundary, Loader } from '@cloudbeaver/core-blocks';
import { DisplayError, ErrorBoundary, Loader, s } from '@cloudbeaver/core-blocks';
import { AppContext, IServiceInjector } from '@cloudbeaver/core-di';
import styles from './renderLayout.m.css';
interface IRender {
initRoot(): Root;
@@ -46,18 +46,14 @@ export function renderLayout(serviceInjector: IServiceInjector): IRender {
},
renderApp() {
this.initRoot().render(
styled`
Loader {
height: 100vh;
}
`(
(
<AppContext app={serviceInjector}>
<ErrorBoundary root>
<Suspense fallback={<Loader />}>
<Suspense fallback={<Loader className={s(styles, { loader: true })} />}>
<Body />
</Suspense>
</ErrorBoundary>
</AppContext>,
</AppContext>
),
);
},
@@ -0,0 +1,3 @@
.container {
outline: none;
}
@@ -8,9 +8,7 @@
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import styled, { css } from 'reshadow';
import { useFocus } from '@cloudbeaver/core-blocks';
import { s, useFocus, useS } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { isObjectsEqual } from '@cloudbeaver/core-utils';
@@ -22,12 +20,7 @@ import type { IView } from './IView';
import { parseHotkey } from './parseHotkey';
import { useActiveView } from './useActiveView';
import { useViewContext } from './useViewContext';
const styles = css`
div {
outline: none;
}
`;
import styles from './CaptureView.m.css';
interface Props {
view: IView<any>;
@@ -40,6 +33,7 @@ export const CaptureView = observer<React.PropsWithChildren<Props>>(function Cap
const actionService = useService(ActionService);
const activeView = useActiveView(view);
const [ref, state] = useFocus<HTMLDivElement>({ onFocus: activeView.focusView, onBlur: activeView.blurView });
const style = useS(styles);
const actionItems = view.actions
.map(action => actionService.getAction(viewContext, action))
@@ -85,11 +79,11 @@ export const CaptureView = observer<React.PropsWithChildren<Props>>(function Cap
},
);
return styled(styles)(
return (
<CaptureViewContext.Provider value={viewContext}>
<div ref={ref} className={className} tabIndex={0}>
<div ref={ref} className={s(style, { container: true }, className)} tabIndex={0}>
{children}
</div>
</CaptureViewContext.Provider>,
</CaptureViewContext.Provider>
);
});
@@ -0,0 +1,30 @@
.wizardStepper {
composes: theme-background-surface theme-text-on-surface theme-border-color-background from global;
position: sticky;
top: 0;
height: 48px;
display: flex;
padding: 0 16px;
align-items: center;
border-bottom: solid 2px;
z-index: 1;
flex-shrink: 0;
}
.actions {
composes: theme-border-color-background from global;
border-right: solid 1px;
display: flex;
gap: 16px;
}
.actions,
.wizardText {
display: flex;
align-items: center;
height: 28px;
}
.wizardText {
padding: 0 16px;
}
@@ -6,60 +6,28 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { ConfigurationWizardService } from '@cloudbeaver/core-administration';
import { Button, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { Button, s, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
const styles = css`
wizard-stepper {
composes: theme-background-surface theme-text-on-surface theme-border-color-background from global;
position: sticky;
top: 0;
height: 48px;
display: flex;
padding: 0 16px;
align-items: center;
border-bottom: solid 2px;
z-index: 1;
flex-shrink: 0;
}
actions {
composes: theme-border-color-background from global;
border-right: solid 1px;
display: flex;
gap: 16px;
}
actions,
wizard-text {
display: flex;
align-items: center;
height: 28px;
}
wizard-text {
padding: 0 16px;
}
`;
import styles from './WizardStepper.m.css';
export const WizardStepper = observer(function WizardStepper() {
const service = useService(ConfigurationWizardService);
const translate = useTranslate();
const style = useS(styles);
return styled(useStyles(styles))(
<wizard-stepper>
<actions>
return (
<div className={s(style, { wizardStepper: true })}>
<div className={s(style, { actions: true })}>
<Button type="button" mod={['outlined']} disabled={service.currentStepIndex === 0} onClick={() => service.back()}>
{translate('ui_stepper_back')}
</Button>
<Button type="button" mod={['unelevated']} onClick={() => service.next()}>
{translate(service.currentStepIndex === service.steps.length - 1 ? 'ui_stepper_finish' : 'ui_stepper_next')}
</Button>
</actions>
<wizard-text>{translate(service.currentStep?.configurationWizardOptions?.description || '')}</wizard-text>
</wizard-stepper>,
</div>
<div className={s(style, { wizardText: true })}>{translate(service.currentStep?.configurationWizardOptions?.description || '')}</div>
</div>
);
});
@@ -0,0 +1,4 @@
.wizardTitle {
text-transform: uppercase;
font-weight: 700;
}
@@ -5,21 +5,14 @@
* 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 { Translate } from '@cloudbeaver/core-blocks';
const styles = css`
wizard-title {
text-transform: uppercase;
font-weight: 700;
}
`;
import { Translate, s, useS } from '@cloudbeaver/core-blocks';
import styles from './ConfigurationWizardTitle.m.css';
export function ConfigurationWizardTitle() {
return styled(styles)(
<wizard-title as="div">
const style = useS(styles);
return (
<div className={s(style, { wizardTitle : true })}>
<Translate token="administration_configuration_wizard_title" />
</wizard-title>,
</div>
);
}
@@ -0,0 +1,15 @@
.form {
flex: 1;
display: flex;
overflow: auto;
flex-direction: column;
}
.p {
white-space: pre-wrap;
line-height: 2;
}
.loader {
height: 100%;
}
@@ -6,9 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { ADMINISTRATION_TOOLS_PANEL_STYLES, AdministrationItemContentComponent, ConfigurationWizardService } from '@cloudbeaver/core-administration';
import { AdministrationItemContentComponent, ConfigurationWizardService } from '@cloudbeaver/core-administration';
import {
ColoredContainer,
ConfirmationDialog,
@@ -21,9 +20,10 @@ import {
Placeholder,
ToolsAction,
ToolsPanel,
s,
useFocus,
useFormValidator,
useStyles,
useS,
useTranslate,
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
@@ -37,28 +37,11 @@ import { ServerConfigurationNavigatorViewForm } from './Form/ServerConfiguration
import { ServerConfigurationSecurityForm } from './Form/ServerConfigurationSecurityForm';
import { ServerConfigurationDriversForm } from './ServerConfigurationDriversForm';
import { ServerConfigurationService } from './ServerConfigurationService';
const styles = css`
Form {
flex: 1;
display: flex;
overflow: auto;
flex-direction: column;
}
p {
white-space: pre-wrap;
line-height: 2;
}
Loader {
height: 100%;
}
`;
import style from './ServerConfigurationPage.m.css';
export const ServerConfigurationPage: AdministrationItemContentComponent = observer(function ServerConfigurationPage({ configurationWizard }) {
const translate = useTranslate();
const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES);
const styles = useS(style);
const [focusedRef, state] = useFocus<HTMLFormElement>({ focusFirstChild: true });
const service = useService(ServerConfigurationService);
const serverConfigResource = useService(ServerConfigResource);
@@ -97,10 +80,10 @@ export const ServerConfigurationPage: AdministrationItemContentComponent = obser
}
}
return styled(style)(
<Form ref={focusedRef} name="server_config" onChange={handleChange}>
return (
<Form ref={focusedRef} className={s(styles, { form: true })} name="server_config" onChange={handleChange}>
{!configurationWizard && (
<ToolsPanel>
<ToolsPanel hasBottomBorder>
<ToolsAction
title={translate('administration_configuration_tools_save_tooltip')}
icon="admin-save"
@@ -128,14 +111,14 @@ export const ServerConfigurationPage: AdministrationItemContentComponent = obser
<h3>{translate('administration_configuration_wizard_configuration_title')}</h3>
</GroupItem>
<GroupItem>
<p>{translate('administration_configuration_wizard_configuration_message')}</p>
<p className={s(styles, { p: true })}>{translate('administration_configuration_wizard_configuration_message')}</p>
</GroupItem>
</Group>
)}
<Loader state={service}>
{() =>
styled(style)(
<Loader suspense>
(
<Loader className={s(styles, { loader: true })} suspense>
<Container wrap gap grid medium>
<ServerConfigurationInfoForm state={service.state} />
<Group form gap>
@@ -149,11 +132,11 @@ export const ServerConfigurationPage: AdministrationItemContentComponent = obser
<ServerConfigurationSecurityForm serverConfig={service.state.serverConfig} />
<ServerConfigurationDriversForm serverConfig={service.state.serverConfig} />
</Container>
</Loader>,
</Loader>
)
}
</Loader>
</ColoredContainer>
</Form>,
</Form>
);
});
@@ -6,68 +6,63 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { ADMINISTRATION_TOOLS_PANEL_STYLES, AdministrationItemContentComponent } from '@cloudbeaver/core-administration';
import { ColoredContainer, Container, Group, Loader, ToolsAction, ToolsPanel, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import type { AdministrationItemContentComponent } from '@cloudbeaver/core-administration';
import { ColoredContainer, Container, ExceptionMessageStyles, Group, Loader, SContext, StyleRegistry, ToolsAction, ToolsPanel, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { AuthConfigurationsTable } from './AuthConfigurationsTable/AuthConfigurationsTable';
import { useConfigurationsTable } from './AuthConfigurationsTable/useConfigurationsTable';
import { CreateAuthConfiguration } from './CreateAuthConfiguration';
import { CreateAuthConfigurationService } from './CreateAuthConfigurationService';
import AuthConfigurationsAdministrationStyle from './AuthConfigurationsAdministration.m.css';
const loaderStyle = css`
ExceptionMessage {
padding: 24px;
}
`;
const styles = css`
ToolsPanel {
border-bottom: none;
}
`;
const registry: StyleRegistry = [[
ExceptionMessageStyles,
{
mode: 'append',
styles: [AuthConfigurationsAdministrationStyle],
},
]];
export const AuthConfigurationsAdministration: AdministrationItemContentComponent = observer(function AuthConfigurationsAdministration({ sub }) {
const translate = useTranslate();
const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES);
const service = useService(CreateAuthConfigurationService);
const table = useConfigurationsTable();
return styled(style)(
return (
<ColoredContainer wrap gap parent vertical>
<Group box keepSize>
<ToolsPanel>
<ToolsAction
title={translate('administration_identity_providers_add_tooltip')}
icon="add"
viewBox="0 0 24 24"
disabled={!!sub || table.processing}
onClick={service.create}
>
{translate('ui_add')}
</ToolsAction>
<ToolsAction
title={translate('administration_identity_providers_refresh_tooltip')}
icon="refresh"
viewBox="0 0 24 24"
disabled={table.processing}
onClick={table.update}
>
{translate('ui_refresh')}
</ToolsAction>
<ToolsAction
title={translate('administration_identity_providers_delete_tooltip')}
icon="trash"
viewBox="0 0 24 24"
disabled={!table.tableState.itemsSelected || table.processing}
onClick={table.delete}
>
{translate('ui_delete')}
</ToolsAction>
</ToolsPanel>
<ToolsPanel>
<ToolsAction
title={translate('administration_identity_providers_add_tooltip')}
icon="add"
viewBox="0 0 24 24"
disabled={!!sub || table.processing}
onClick={service.create}
>
{translate('ui_add')}
</ToolsAction>
<ToolsAction
title={translate('administration_identity_providers_refresh_tooltip')}
icon="refresh"
viewBox="0 0 24 24"
disabled={table.processing}
onClick={table.update}
>
{translate('ui_refresh')}
</ToolsAction>
<ToolsAction
title={translate('administration_identity_providers_delete_tooltip')}
icon="trash"
viewBox="0 0 24 24"
disabled={!table.tableState.itemsSelected || table.processing}
onClick={table.delete}
>
{translate('ui_delete')}
</ToolsAction>
</ToolsPanel>
</Group>
<Container overflow gap>
{sub && (
@@ -76,15 +71,17 @@ export const AuthConfigurationsAdministration: AdministrationItemContentComponen
</Group>
)}
<Group boxNoOverflow>
<Loader style={loaderStyle} loading={table.processing} overlay>
<AuthConfigurationsTable
configurations={table.configurations}
selectedItems={table.tableState.selected}
expandedItems={table.tableState.expanded}
/>
</Loader>
<SContext registry={registry}>
<Loader loading={table.processing} overlay>
<AuthConfigurationsTable
configurations={table.configurations}
selectedItems={table.tableState.selected}
expandedItems={table.tableState.expanded}
/>
</Loader>
</SContext>
</Group>
</Container>
</ColoredContainer>,
</ColoredContainer>
);
});
@@ -0,0 +1,28 @@
.group {
position: relative;
}
.group,
.container,
.tableContainer {
height: 100%;
}
.container {
display: flex;
flex-direction: column;
width: 100%;
}
.tableContainer {
overflow: auto;
}
.grantedConnectionsTableHeader {
flex: 0 0 auto;
}
.table {
composes: theme-background-surface theme-text-on-surface from global;
width: 100%;
}
@@ -8,19 +8,19 @@
import { observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useCallback, useState } from 'react';
import styled, { css } from 'reshadow';
import {
Button,
getComputed,
getSelectedItems,
Group,
s,
Table,
TableBody,
TableColumnValue,
TableItem,
useObjectRef,
useStyles,
useS,
useTranslate,
} from '@cloudbeaver/core-blocks';
import { Connection, DBDriverResource } from '@cloudbeaver/core-connections';
@@ -30,32 +30,7 @@ import { getFilteredConnections } from './getFilteredConnections';
import { GrantedConnectionsTableHeader, IFilterState } from './GrantedConnectionsTableHeader/GrantedConnectionsTableHeader';
import { GrantedConnectionsTableInnerHeader } from './GrantedConnectionsTableHeader/GrantedConnectionsTableInnerHeader';
import { GrantedConnectionsTableItem } from './GrantedConnectionsTableItem';
const styles = css`
Group {
position: relative;
}
Group,
container,
table-container {
height: 100%;
}
container {
display: flex;
flex-direction: column;
width: 100%;
}
table-container {
overflow: auto;
}
GrantedConnectionsTableHeader {
flex: 0 0 auto;
}
Table {
composes: theme-background-surface theme-text-on-surface from global;
width: 100%;
}
`;
import styles from './ConnectionList.m.css';
interface Props {
connectionList: Connection[];
@@ -66,7 +41,7 @@ interface Props {
export const ConnectionList = observer<Props>(function ConnectionList({ connectionList, grantedSubjects, disabled, onGrant }) {
const props = useObjectRef({ onGrant });
const style = useStyles(styles);
const style = useS(styles);
const translate = useTranslate();
const driversResource = useService(DBDriverResource);
@@ -84,17 +59,17 @@ export const ConnectionList = observer<Props>(function ConnectionList({ connecti
const connections = getFilteredConnections(connectionList, filterState.filterValue);
const keys = connections.map(connection => connection.id);
return styled(style)(
<Group box medium overflow>
<container>
return (
<Group className={s(style, { group: true })} box medium overflow>
<div className={s(style, { container: true })}>
<GrantedConnectionsTableHeader filterState={filterState} disabled={disabled}>
<Button disabled={disabled || !selected} mod={['unelevated']} onClick={grant}>
{translate('ui_add')}
</Button>
</GrantedConnectionsTableHeader>
<table-container>
<Table keys={keys} selectedItems={selectedSubjects} isItemSelectable={item => !grantedSubjects.includes(item)} size="big">
<GrantedConnectionsTableInnerHeader disabled={disabled} />
<div className={s(style, { tableContainer: true })}>
<Table className={s(style, { table: true })} keys={keys} selectedItems={selectedSubjects} isItemSelectable={item => !grantedSubjects.includes(item)} size="big">
<GrantedConnectionsTableInnerHeader className={s(style, { grantedConnectionsTableHeader: true })} disabled={disabled} />
<TableBody>
{!connections.length && filterState.filterValue && (
<TableItem item="tableInfo" selectDisabled>
@@ -117,8 +92,8 @@ export const ConnectionList = observer<Props>(function ConnectionList({ connecti
})}
</TableBody>
</Table>
</table-container>
</container>
</Group>,
</div>
</div>
</Group>
);
});
@@ -6,10 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { ADMINISTRATION_TOOLS_PANEL_STYLES, IAdministrationItemSubItem } from '@cloudbeaver/core-administration';
import { ColoredContainer, Container, Group, ToolsAction, ToolsPanel, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { ColoredContainer, Container, Group, ToolsAction, ToolsPanel, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CreateTeam } from './CreateTeam';
@@ -17,26 +15,18 @@ import { CreateTeamService } from './CreateTeamService';
import { TeamsTable } from './TeamsTable/TeamsTable';
import { useTeamsTable } from './TeamsTable/useTeamsTable';
const styles = css`
ToolsPanel {
border-bottom: none;
}
`;
interface Props {
sub?: IAdministrationItemSubItem;
param?: string | null;
}
export const TeamsPage = observer<Props>(function TeamsPage({ sub, param }) {
export const TeamsPage = observer<Props>(function TeamsPage({ param }) {
const translate = useTranslate();
const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES);
const service = useService(CreateTeamService);
const table = useTeamsTable();
const create = param === 'create';
return styled(style)(
return (
<ColoredContainer vertical wrap gap parent>
<Group box keepSize>
<ToolsPanel>
@@ -80,6 +70,6 @@ export const TeamsPage = observer<Props>(function TeamsPage({ sub, param }) {
<TeamsTable teams={table.teams} state={table.state} selectedItems={table.tableState.selected} expandedItems={table.tableState.expanded} />
</Group>
</Container>
</ColoredContainer>,
</ColoredContainer>
);
});
@@ -6,19 +6,21 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { css } from 'reshadow';
import type { TeamInfo } from '@cloudbeaver/core-authentication';
import { Loader, Table, TableBody, TableColumnHeader, TableHeader, TableSelect, useTranslate } from '@cloudbeaver/core-blocks';
import { ExceptionMessageStyles, Loader, SContext, StyleRegistry, Table, TableBody, TableColumnHeader, TableHeader, TableSelect, useTranslate } from '@cloudbeaver/core-blocks';
import type { ILoadableState } from '@cloudbeaver/core-utils';
import { Team } from './Team';
import teamsTableStyle from './TeamsTable.m.css';
const loaderStyle = css`
ExceptionMessage {
padding: 24px;
}
`;
const registry: StyleRegistry = [[
ExceptionMessageStyles,
{
mode: 'append',
styles: [teamsTableStyle],
},
]];
interface Props {
teams: TeamInfo[];
@@ -32,24 +34,26 @@ export const TeamsTable = observer<Props>(function TeamsTable({ teams, state, se
const keys = teams.map(team => team.teamId);
return (
<Loader state={state} style={loaderStyle} overlay>
<Table keys={keys} selectedItems={selectedItems} expandedItems={expandedItems} size="big">
<TableHeader fixed>
<TableColumnHeader min flex centerContent>
<TableSelect />
</TableColumnHeader>
<TableColumnHeader min />
<TableColumnHeader>{translate('administration_teams_team_id')}</TableColumnHeader>
<TableColumnHeader>{translate('administration_teams_team_name')}</TableColumnHeader>
<TableColumnHeader>{translate('administration_teams_team_description')}</TableColumnHeader>
<TableColumnHeader />
</TableHeader>
<TableBody>
{teams.map(team => (
<Team key={team.teamId} team={team} />
))}
</TableBody>
</Table>
</Loader>
<SContext registry={registry}>
<Loader state={state} overlay>
<Table keys={keys} selectedItems={selectedItems} expandedItems={expandedItems} size="big">
<TableHeader fixed>
<TableColumnHeader min flex centerContent>
<TableSelect />
</TableColumnHeader>
<TableColumnHeader min />
<TableColumnHeader>{translate('administration_teams_team_id')}</TableColumnHeader>
<TableColumnHeader>{translate('administration_teams_team_name')}</TableColumnHeader>
<TableColumnHeader>{translate('administration_teams_team_description')}</TableColumnHeader>
<TableColumnHeader />
</TableHeader>
<TableBody>
{teams.map(team => (
<Team key={team.teamId} team={team} />
))}
</TableBody>
</Table>
</Loader>
</SContext>
);
});
@@ -8,7 +8,7 @@
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { ADMINISTRATION_TOOLS_PANEL_STYLES, AdministrationItemContentComponent } from '@cloudbeaver/core-administration';
import type { AdministrationItemContentComponent } from '@cloudbeaver/core-administration';
import { s, ToolsPanel, useS, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { BASE_TAB_STYLES, ITabData, Tab, TabList, TabPanel, TabsState, TabTitle, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui';
@@ -28,7 +28,7 @@ const tabsStyles = css`
}
`;
const tabStyle = [ADMINISTRATION_TOOLS_PANEL_STYLES, BASE_TAB_STYLES, tabsStyles, UNDERLINE_TAB_STYLES];
const tabStyle = [BASE_TAB_STYLES, tabsStyles, UNDERLINE_TAB_STYLES];
export const UsersAdministration: AdministrationItemContentComponent = observer(function UsersAdministration({ sub, param }) {
const translate = useTranslate();
@@ -48,7 +48,7 @@ export const UsersAdministration: AdministrationItemContentComponent = observer(
return styled(useStyles(tabStyle))(
<TabsState selectedId={subName} lazy onChange={openSub}>
<ToolsPanel className={s(styles, { tabPanel: true })}>
<ToolsPanel className={s(styles, { tabPanel: true })} hasBottomBorder>
<TabList className={s(styles, { tabList: true })} aria-label="User Administration pages" style={tabStyle}>
<Tab className={s(styles, { tab: true })} tabId={EUsersAdministrationSub.Users} style={tabStyle}>
<TabTitle className={s(styles, { tabTitle: true })}>{translate('authentication_administration_item_users')}</TabTitle>
@@ -59,10 +59,10 @@ export const UsersAdministration: AdministrationItemContentComponent = observer(
</TabList>
</ToolsPanel>
<TabPanel className={s(styles, { tabPanel: true })} tabId={EUsersAdministrationSub.Users}>
<UsersPage sub={sub} param={param} />
<UsersPage param={param} />
</TabPanel>
<TabPanel className={s(styles, { tabPanel: true })} tabId={EUsersAdministrationSub.Teams}>
<TeamsPage sub={sub} param={param} />
<TeamsPage param={param} />
</TabPanel>
</TabsState>,
);
@@ -1,5 +1,4 @@
.toolsPanel {
border-bottom: none;
border-radius: inherit;
& .menuBar {
@@ -6,11 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import { ADMINISTRATION_TOOLS_PANEL_STYLES, IAdministrationItemSubItem } from '@cloudbeaver/core-administration';
import { AuthRolesResource } from '@cloudbeaver/core-authentication';
import { ColoredContainer, Container, Group, Placeholder, useAutoLoad, useResource, useStyles } from '@cloudbeaver/core-blocks';
import { ColoredContainer, Container, Group, Placeholder, useAutoLoad, useResource } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService';
@@ -23,12 +20,10 @@ import { UsersTable } from './UsersTable';
import { useUsersTable } from './useUsersTable';
interface Props {
sub?: IAdministrationItemSubItem;
param?: string | null;
}
export const UsersPage = observer<Props>(function UsersPage({ sub, param }) {
const style = useStyles(ADMINISTRATION_TOOLS_PANEL_STYLES);
export const UsersPage = observer<Props>(function UsersPage({ param }) {
const createUserService = useService(CreateUserService);
const authRolesResource = useResource(UsersPage, AuthRolesResource, undefined);
const administrationUsersManagementService = useService(AdministrationUsersManagementService);
@@ -42,7 +37,7 @@ export const UsersPage = observer<Props>(function UsersPage({ sub, param }) {
const loading = authRolesResource.isLoading() || table.loadableState.isLoading();
const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled;
return styled(style)(
return (
<ColoredContainer vertical wrap gap parent>
<Group box keepSize>
<UsersAdministrationToolsPanel onUpdate={table.update} />
@@ -73,6 +68,6 @@ export const UsersPage = observer<Props>(function UsersPage({ sub, param }) {
/>
</Group>
</Container>
</ColoredContainer>,
</ColoredContainer>
);
});
@@ -0,0 +1,7 @@
.groupItem {
white-space: pre-wrap;
}
.error {
padding: 24px;
}
@@ -6,20 +6,23 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { ADMINISTRATION_TOOLS_PANEL_STYLES, AdministrationItemContentProps } from '@cloudbeaver/core-administration';
import type { AdministrationItemContentProps } from '@cloudbeaver/core-administration';
import {
ColoredContainer,
Container,
ExceptionMessageStyles,
Group,
GroupItem,
GroupTitle,
Loader,
SContext,
StyleRegistry,
ToolsAction,
ToolsPanel,
s,
useResource,
useStyles,
useS,
useTranslate,
} from '@cloudbeaver/core-blocks';
import { ConnectionInfoActiveProjectKey, ConnectionInfoResource, DBDriverResource } from '@cloudbeaver/core-connections';
@@ -30,22 +33,15 @@ import { ConnectionsAdministrationController } from './ConnectionsAdministration
import { ConnectionsTable } from './ConnectionsTable/ConnectionsTable';
import { CreateConnection } from './CreateConnection/CreateConnection';
import { CreateConnectionService } from './CreateConnectionService';
import ConnectionsAdministrationStyle from './ConnectionsAdministration.m.css';
const loaderStyle = css`
ExceptionMessage {
padding: 24px;
}
`;
const styles = css`
GroupItem {
white-space: pre-wrap;
}
ToolsPanel {
border-bottom: none;
}
`;
const registry: StyleRegistry = [[
ExceptionMessageStyles,
{
mode: 'append',
styles: [ConnectionsAdministrationStyle],
},
]];
export const ConnectionsAdministration = observer<AdministrationItemContentProps>(function ConnectionsAdministration({
sub,
@@ -55,7 +51,7 @@ export const ConnectionsAdministration = observer<AdministrationItemContentProps
const service = useService(CreateConnectionService);
const controller = useController(ConnectionsAdministrationController);
const translate = useTranslate();
const style = useStyles(styles, ADMINISTRATION_TOOLS_PANEL_STYLES);
const style = useS(ConnectionsAdministrationStyle);
useResource(ConnectionsAdministration, ConnectionInfoResource, {
key: ConnectionInfoActiveProjectKey,
@@ -63,7 +59,7 @@ export const ConnectionsAdministration = observer<AdministrationItemContentProps
});
useResource(ConnectionsAdministration, DBDriverResource, CachedMapAllKey);
return styled(style)(
return (
<ColoredContainer vertical wrap parent gap>
<Group box keepSize>
<ToolsPanel>
@@ -100,7 +96,7 @@ export const ConnectionsAdministration = observer<AdministrationItemContentProps
{configurationWizard && (
<Group gap>
<GroupTitle>{translate('connections_administration_configuration_wizard_title')}</GroupTitle>
<GroupItem>{translate('connections_administration_configuration_wizard_message')}</GroupItem>
<GroupItem className={s(style, { groupItem: true })}>{translate('connections_administration_configuration_wizard_message')}</GroupItem>
</Group>
)}
{sub && (
@@ -109,16 +105,18 @@ export const ConnectionsAdministration = observer<AdministrationItemContentProps
</Group>
)}
<Group boxNoOverflow>
<Loader style={loaderStyle} loading={controller.isProcessing} overlay>
<ConnectionsTable
keys={controller.keys}
connections={controller.connections}
selectedItems={controller.selectedItems}
expandedItems={controller.expandedItems}
/>
</Loader>
<SContext registry={registry}>
<Loader loading={controller.isProcessing} overlay>
<ConnectionsTable
keys={controller.keys}
connections={controller.connections}
selectedItems={controller.selectedItems}
expandedItems={controller.expandedItems}
/>
</Loader>
</SContext>
</Group>
</Container>
</ColoredContainer>,
</ColoredContainer>
);
});
@@ -20,13 +20,6 @@ import { TableFooterMenu } from './TableFooterMenu/TableFooterMenu';
import { TableFooterRowCount } from './TableFooterRowCount';
const tableFooterStyles = css`
ToolsPanel {
align-items: center;
flex: 0 0 auto;
overflow: auto;
min-height: 32px;
height: initial;
}
count input,
count placeholder {
height: 26px;
@@ -103,7 +96,7 @@ export const TableFooter = observer<Props>(function TableFooter({ resultIndex, m
const disabled = getComputed(() => model.isLoading() || model.isDisabled(resultIndex));
return styled(tableFooterStyles)(
<ToolsPanel>
<ToolsPanel type="secondary">
{/* <reload aria-disabled={disabled} onClick={() => model.refresh()}>
<IconOrImage icon='reload' viewBox="0 0 16 16" />
</reload> */}
@@ -130,6 +123,6 @@ export const TableFooter = observer<Props>(function TableFooter({ resultIndex, m
{model.source.requestInfo.requestMessage} - {model.source.requestInfo.requestDuration}ms
</time>
)}
</ToolsPanel>,
</ToolsPanel>
);
});
@@ -0,0 +1,10 @@
.group {
list-style-position: inside;
height: 100%;
max-height: 300px;
}
.reactMarkdown > * {
margin: 0;
padding: 0;
}
@@ -7,44 +7,32 @@
*/
import { observer } from 'mobx-react-lite';
import ReactMarkdown from 'react-markdown';
import styled, { css } from 'reshadow';
import { Container, Group, GroupItem, GroupTitle, useResource, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { Container, Group, GroupItem, GroupTitle, s, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { VersionResource } from '@cloudbeaver/core-version';
import styles from './VersionInfo.m.css';
interface Props {
item: string;
}
const style = css`
Group {
list-style-position: inside;
height: 100%;
max-height: 300px;
}
ReactMarkdown > * {
margin: 0;
padding: 0;
}
`;
export const VersionInfo = observer<Props>(function VersionInfo({ item }) {
const translate = useTranslate();
const styles = useStyles(style);
const style = useS(styles);
const versionResource = useResource(VersionInfo, VersionResource, item);
const version = versionResource.tryGetData;
return styled(styles)(
return (
<Container wrap gap overflow large>
<Group gap overflow>
<Group className={s(style, { group: true })} gap overflow>
<GroupTitle>{version ? `Release notes ${version.number} - ${version.date}` : translate('version_update_version_no_info')}</GroupTitle>
{version && (
<GroupItem>
<ReactMarkdown>{version.releaseNotes}</ReactMarkdown>
<ReactMarkdown className={s(style, { reactMarkdown: true })}>{version.releaseNotes}</ReactMarkdown>
</GroupItem>
)}
</Group>
</Container>,
</Container>
);
});
@@ -0,0 +1,11 @@
.group {
list-style-position: inside;
}
.instruction {
white-space: pre-line;
}
.h4 {
margin: 0;
}
@@ -7,37 +7,26 @@
*/
import { observer } from 'mobx-react-lite';
import { useEffect, useState } from 'react';
import styled, { css } from 'reshadow';
import { Combobox, Container, Group, GroupItem, GroupTitle, useTranslate } from '@cloudbeaver/core-blocks';
import { Combobox, Container, Group, GroupItem, GroupTitle, s, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { ServerConfigResource } from '@cloudbeaver/core-root';
import { IVersion, VersionResource } from '@cloudbeaver/core-version';
import { VersionUpdateService } from '@cloudbeaver/core-version-update';
import { VersionInfo } from './VersionInfo';
import styles from './VersionSelector.m.css';
interface Props {
versions: IVersion[];
}
const style = css`
Group {
list-style-position: inside;
}
Instruction {
white-space: pre-line;
}
h4 {
margin: 0;
}
`;
export const VersionSelector = observer<Props>(function VersionSelector({ versions }) {
const versionUpdateService = useService(VersionUpdateService);
const versionResource = useService(VersionResource);
const serverConfigResource = useService(ServerConfigResource);
const translate = useTranslate();
const style = useS(styles);
const [selected, setSelected] = useState('');
@@ -50,9 +39,9 @@ export const VersionSelector = observer<Props>(function VersionSelector({ versio
const version = versions.find(v => v.number === selected);
const Instruction = versionUpdateService.versionInstructionGetter?.();
return styled(style)(
return (
<Container gap>
<Group gap large>
<Group className={s(style, { group: true })} gap large>
<Combobox
items={versions}
keySelector={value => value.number}
@@ -65,15 +54,15 @@ export const VersionSelector = observer<Props>(function VersionSelector({ versio
</Combobox>
{version && Instruction && (
<GroupItem>
<Instruction version={version} containerId={serverConfigResource.data?.containerId} />
<Instruction className={s(style, { instruction: true })} version={version} containerId={serverConfigResource.data?.containerId} />
</GroupItem>
)}
<GroupTitle>{translate('plugin_version_update_administration_recommendations_label')}</GroupTitle>
<GroupItem>
<h4>{translate('plugin_version_update_administration_recommendations')}</h4>
<h4 className={s(style, { h4: true })}>{translate('plugin_version_update_administration_recommendations')}</h4>
</GroupItem>
</Group>
{version && <VersionInfo item={version.number} />}
</Container>,
</Container>
);
});
@@ -0,0 +1,4 @@
.coloredContainer {
composes: theme-typography--body2 from global;
list-style-position: inside;
}
@@ -6,11 +6,10 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { gte } from 'semver';
import type { AdministrationItemContentComponent } from '@cloudbeaver/core-administration';
import { ColoredContainer, useResource, useStyles } from '@cloudbeaver/core-blocks';
import { ColoredContainer, s, useResource, useS, useStyles } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CachedMapAllKey } from '@cloudbeaver/core-resource';
import { VersionResource, VersionService } from '@cloudbeaver/core-version';
@@ -18,16 +17,10 @@ import { VersionUpdateService } from '@cloudbeaver/core-version-update';
import { VersionChecker } from './VersionChecker';
import { VersionSelector } from './VersionSelector';
const styles = css`
ColoredContainer {
composes: theme-typography--body2 from global;
list-style-position: inside;
}
`;
import styles from './VersionUpdate.m.css';
export const VersionUpdate: AdministrationItemContentComponent = observer(function VersionUpdate() {
const style = useStyles(styles);
const style = useS(styles);
const versionService = useService(VersionService);
const versionUpdateService = useService(VersionUpdateService);
const versionResource = useResource(VersionUpdate, VersionResource, CachedMapAllKey, {
@@ -37,8 +30,8 @@ export const VersionUpdate: AdministrationItemContentComponent = observer(functi
const GeneralInstructions = versionUpdateService.generalInstructionsGetter?.();
const versions = versionResource.resource.values.filter(v => gte(v.number, versionService.current));
return styled(style)(
<ColoredContainer wrap gap overflow parent>
return (
<ColoredContainer className={s(style, { coloredContainer: true })} wrap gap overflow parent>
<VersionChecker />
{versions.length > 0 && (
<>
@@ -46,6 +39,6 @@ export const VersionUpdate: AdministrationItemContentComponent = observer(functi
<VersionSelector versions={versions} />
</>
)}
</ColoredContainer>,
</ColoredContainer>
);
});