mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge pull request #449 from dbeaver/feat/identity-providers-public
feat(plugin-authentication): CB-1062 add support for identity provide…
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 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 styled, { css } from 'reshadow';
|
||||
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
interface Props {
|
||||
description?: string;
|
||||
before?: JSX.Element;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
cell {
|
||||
composes: theme-ripple from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
}
|
||||
before {
|
||||
margin-right: 12px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
info {
|
||||
composes: theme-typography--body2 from global;
|
||||
line-height: 1.4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: 500;
|
||||
}
|
||||
description {
|
||||
composes: theme-typography--caption from global;
|
||||
line-height: 1.2;
|
||||
}
|
||||
`);
|
||||
|
||||
export const Cell: React.FC<Props> = function Cell({ before, description, className, children }) {
|
||||
return styled(useStyles(styles))(
|
||||
<cell className={className}>
|
||||
<main>
|
||||
<before>{before && before}</before>
|
||||
<info>
|
||||
{children}
|
||||
{description && <description>{description}</description>}
|
||||
</info>
|
||||
</main>
|
||||
</cell>
|
||||
);
|
||||
};
|
||||
@@ -25,6 +25,9 @@ const styles = css`
|
||||
InputFieldNew {
|
||||
display: none;
|
||||
width: 300px;
|
||||
&[|max] {
|
||||
width: 100%;
|
||||
}
|
||||
&[|toggled] {
|
||||
display: block;
|
||||
}
|
||||
@@ -67,6 +70,7 @@ interface BaseProps {
|
||||
toggleMode?: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
max?: boolean;
|
||||
className?: string;
|
||||
onToggle?: (status: boolean) => void;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
@@ -93,6 +97,7 @@ export const Filter: React.FC<ControlledProps | ObjectsProps<any, any>> = observ
|
||||
toggleMode,
|
||||
placeholder,
|
||||
disabled,
|
||||
max,
|
||||
className,
|
||||
onFilter,
|
||||
onToggle,
|
||||
@@ -152,7 +157,7 @@ export const Filter: React.FC<ControlledProps | ObjectsProps<any, any>> = observ
|
||||
value={value}
|
||||
onChange={filter}
|
||||
onKeyDown={onKeyDown}
|
||||
{...use({ toggled })}
|
||||
{...use({ toggled, max })}
|
||||
/>
|
||||
<IconButton
|
||||
name='search'
|
||||
|
||||
@@ -6,14 +6,31 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export const Link: React.FC<React.AnchorHTMLAttributes<HTMLAnchorElement>> = function Link({
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
interface Props extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
wrapper?: boolean;
|
||||
}
|
||||
|
||||
const styles = css`
|
||||
a[|wrapper] {
|
||||
&, &:hover, &:focus, &:active {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const Link: React.FC<Props> = function Link({
|
||||
wrapper,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
}) {
|
||||
return (
|
||||
return styled(styles)(
|
||||
<div className={className}>
|
||||
<a {...rest}>{children}</a>
|
||||
<a {...use({ wrapper })} {...rest}>{children}</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -127,6 +127,7 @@ export * from './FormControls/SubmittingForm';
|
||||
export * from './FormControls/Textarea';
|
||||
export * from './FormControls/TextareaNew';
|
||||
export * from './Link';
|
||||
export * from './Cell';
|
||||
export * from './ErrorMessage';
|
||||
export * from './StatusMessage';
|
||||
export * from './ExceptionMessage';
|
||||
|
||||
@@ -18,6 +18,16 @@ query getAuthProviders {
|
||||
# features
|
||||
#}
|
||||
|
||||
configurable
|
||||
configurations {
|
||||
id
|
||||
displayName
|
||||
iconURL
|
||||
description
|
||||
signInLink
|
||||
signOutLink
|
||||
}
|
||||
|
||||
credentialParameters {
|
||||
id
|
||||
displayName
|
||||
|
||||
@@ -16,6 +16,15 @@ export interface Scalars {
|
||||
Object: any;
|
||||
}
|
||||
|
||||
export interface AdminAuthProviderConfiguration {
|
||||
provider: Scalars['ID'];
|
||||
id: Scalars['ID'];
|
||||
displayName: Scalars['String'];
|
||||
iconURL?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
parameters: Scalars['Object'];
|
||||
}
|
||||
|
||||
export interface AdminConnectionGrantInfo {
|
||||
connectionId: Scalars['ID'];
|
||||
subjectId: Scalars['ID'];
|
||||
@@ -81,19 +90,30 @@ export interface AuthCredentialInfo {
|
||||
id: Scalars['ID'];
|
||||
displayName: Scalars['String'];
|
||||
description?: Maybe<Scalars['String']>;
|
||||
admin?: Maybe<Scalars['Boolean']>;
|
||||
user?: Maybe<Scalars['Boolean']>;
|
||||
identifying?: Maybe<Scalars['Boolean']>;
|
||||
admin: Scalars['Boolean'];
|
||||
user: Scalars['Boolean'];
|
||||
identifying: Scalars['Boolean'];
|
||||
possibleValues?: Maybe<Array<Maybe<Scalars['String']>>>;
|
||||
encryption?: Maybe<AuthCredentialEncryption>;
|
||||
}
|
||||
|
||||
export interface AuthProviderConfiguration {
|
||||
id: Scalars['ID'];
|
||||
displayName: Scalars['String'];
|
||||
iconURL?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
signInLink?: Maybe<Scalars['String']>;
|
||||
signOutLink?: Maybe<Scalars['String']>;
|
||||
}
|
||||
|
||||
export interface AuthProviderInfo {
|
||||
id: Scalars['ID'];
|
||||
label: Scalars['String'];
|
||||
icon?: Maybe<Scalars['ID']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
defaultProvider?: Maybe<Scalars['Boolean']>;
|
||||
defaultProvider: Scalars['Boolean'];
|
||||
configurable: Scalars['Boolean'];
|
||||
configurations?: Maybe<AuthProviderConfiguration[]>;
|
||||
credentialParameters: AuthCredentialInfo[];
|
||||
}
|
||||
|
||||
@@ -579,6 +599,7 @@ export interface Query {
|
||||
dataTransferExportDataFromContainer: AsyncTaskInfo;
|
||||
dataTransferExportDataFromResults: AsyncTaskInfo;
|
||||
dataTransferRemoveDataFile?: Maybe<Scalars['Boolean']>;
|
||||
deleteAuthProviderConfiguration: Scalars['Boolean'];
|
||||
deleteConnectionConfiguration?: Maybe<Scalars['Boolean']>;
|
||||
deleteRole?: Maybe<Scalars['Boolean']>;
|
||||
deleteUser?: Maybe<Scalars['Boolean']>;
|
||||
@@ -586,6 +607,8 @@ export interface Query {
|
||||
getConnectionSubjectAccess: AdminConnectionGrantInfo[];
|
||||
getSubjectConnectionAccess: AdminConnectionGrantInfo[];
|
||||
grantUserRole?: Maybe<Scalars['Boolean']>;
|
||||
listAuthProviderConfigurationParameters: ObjectPropertyInfo[];
|
||||
listAuthProviderConfigurations: AdminAuthProviderConfiguration[];
|
||||
listPermissions: Array<Maybe<AdminPermissionInfo>>;
|
||||
listRoles: Array<Maybe<AdminRoleInfo>>;
|
||||
listUsers: Array<Maybe<AdminUserInfo>>;
|
||||
@@ -597,6 +620,7 @@ export interface Query {
|
||||
networkHandlers: NetworkHandlerDescriptor[];
|
||||
readSessionLog: LogEntry[];
|
||||
revokeUserRole?: Maybe<Scalars['Boolean']>;
|
||||
saveAuthProviderConfiguration: Scalars['Boolean'];
|
||||
searchConnections: AdminConnectionSearchInfo[];
|
||||
serverConfig: ServerConfig;
|
||||
sessionPermissions: Array<Maybe<Scalars['ID']>>;
|
||||
@@ -611,6 +635,7 @@ export interface Query {
|
||||
sqlListContexts: Array<Maybe<SqlContextInfo>>;
|
||||
sqlSupportedOperations: DataTypeLogicalOperation[];
|
||||
templateConnections: ConnectionInfo[];
|
||||
tryFederatedLogin: UserAuthToken;
|
||||
updateConnectionConfiguration: ConnectionInfo;
|
||||
updateRole: AdminRoleInfo;
|
||||
userConnections: ConnectionInfo[];
|
||||
@@ -627,12 +652,14 @@ export interface QueryAuthChangeLocalPasswordArgs {
|
||||
|
||||
export interface QueryAuthLoginArgs {
|
||||
provider: Scalars['ID'];
|
||||
configuration?: Maybe<Scalars['ID']>;
|
||||
credentials: Scalars['Object'];
|
||||
linkUser?: Maybe<Scalars['Boolean']>;
|
||||
}
|
||||
|
||||
export interface QueryAuthLogoutArgs {
|
||||
provider?: Maybe<Scalars['ID']>;
|
||||
configuration?: Maybe<Scalars['ID']>;
|
||||
}
|
||||
|
||||
export interface QueryConfigureServerArgs {
|
||||
@@ -683,6 +710,10 @@ export interface QueryDataTransferRemoveDataFileArgs {
|
||||
dataFileId: Scalars['String'];
|
||||
}
|
||||
|
||||
export interface QueryDeleteAuthProviderConfigurationArgs {
|
||||
id: Scalars['ID'];
|
||||
}
|
||||
|
||||
export interface QueryDeleteConnectionConfigurationArgs {
|
||||
id: Scalars['ID'];
|
||||
}
|
||||
@@ -712,6 +743,14 @@ export interface QueryGrantUserRoleArgs {
|
||||
roleId: Scalars['ID'];
|
||||
}
|
||||
|
||||
export interface QueryListAuthProviderConfigurationParametersArgs {
|
||||
providerId: Scalars['ID'];
|
||||
}
|
||||
|
||||
export interface QueryListAuthProviderConfigurationsArgs {
|
||||
providerId?: Maybe<Scalars['ID']>;
|
||||
}
|
||||
|
||||
export interface QueryListRolesArgs {
|
||||
roleId?: Maybe<Scalars['ID']>;
|
||||
}
|
||||
@@ -755,6 +794,15 @@ export interface QueryRevokeUserRoleArgs {
|
||||
roleId: Scalars['ID'];
|
||||
}
|
||||
|
||||
export interface QuerySaveAuthProviderConfigurationArgs {
|
||||
providerId: Scalars['ID'];
|
||||
id: Scalars['ID'];
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
iconURL?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
parameters?: Maybe<Scalars['Object']>;
|
||||
}
|
||||
|
||||
export interface QuerySearchConnectionsArgs {
|
||||
hostNames: Array<Scalars['String']>;
|
||||
}
|
||||
@@ -809,6 +857,10 @@ export interface QuerySqlSupportedOperationsArgs {
|
||||
attributeIndex: Scalars['Int'];
|
||||
}
|
||||
|
||||
export interface QueryTryFederatedLoginArgs {
|
||||
provider: Scalars['ID'];
|
||||
}
|
||||
|
||||
export interface QueryUpdateConnectionConfigurationArgs {
|
||||
id: Scalars['ID'];
|
||||
config: ConnectionConfig;
|
||||
@@ -1013,6 +1065,7 @@ export interface SessionInfo {
|
||||
|
||||
export interface UserAuthToken {
|
||||
authProvider: Scalars['ID'];
|
||||
authConfiguration?: Maybe<Scalars['ID']>;
|
||||
loginTime: Scalars['DateTime'];
|
||||
userId: Scalars['String'];
|
||||
displayName: Scalars['String'];
|
||||
@@ -1075,8 +1128,8 @@ export type GetAuthProvidersQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export interface GetAuthProvidersQuery {
|
||||
providers: Array<(
|
||||
Pick<AuthProviderInfo, 'id' | 'label' | 'icon' | 'description' | 'defaultProvider'>
|
||||
& { credentialParameters: Array<Pick<AuthCredentialInfo, 'id' | 'displayName' | 'description' | 'admin' | 'user' | 'identifying' | 'possibleValues' | 'encryption'>> }
|
||||
Pick<AuthProviderInfo, 'id' | 'label' | 'icon' | 'description' | 'defaultProvider' | 'configurable'>
|
||||
& { configurations?: Maybe<Array<Pick<AuthProviderConfiguration, 'id' | 'displayName' | 'iconURL' | 'description' | 'signInLink' | 'signOutLink'>>>; credentialParameters: Array<Pick<AuthCredentialInfo, 'id' | 'displayName' | 'description' | 'admin' | 'user' | 'identifying' | 'possibleValues' | 'encryption'>> }
|
||||
)>;
|
||||
}
|
||||
|
||||
@@ -1977,6 +2030,15 @@ export const GetAuthProvidersDocument = `
|
||||
icon
|
||||
description
|
||||
defaultProvider
|
||||
configurable
|
||||
configurations {
|
||||
id
|
||||
displayName
|
||||
iconURL
|
||||
description
|
||||
signInLink
|
||||
signOutLink
|
||||
}
|
||||
credentialParameters {
|
||||
id
|
||||
displayName
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { computed } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import styled, { css } from 'reshadow';
|
||||
import { useMemo } from 'react';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { AdministrationScreenService } from '@cloudbeaver/core-administration';
|
||||
import { UserInfoResource } from '@cloudbeaver/core-authentication';
|
||||
@@ -22,6 +24,7 @@ import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
import { AuthDialogController } from './AuthDialogController';
|
||||
import { AuthDialogFooter } from './AuthDialogFooter';
|
||||
import { AuthProviderForm } from './AuthProviderForm/AuthProviderForm';
|
||||
import { ConfigurationsList } from './AuthProviderForm/ConfigurationsList';
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
@@ -33,10 +36,13 @@ const styles = composes(
|
||||
CommonDialogWrapper {
|
||||
min-height: 400px;
|
||||
min-width: 500px;
|
||||
max-width: min-content;
|
||||
}
|
||||
SubmittingForm {
|
||||
overflow: auto;
|
||||
margin: auto;
|
||||
&[|form] {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
SubmittingForm, AuthProviderForm {
|
||||
flex: 1;
|
||||
@@ -56,6 +62,9 @@ const styles = composes(
|
||||
flex-direction: column;
|
||||
padding: 18px 24px;
|
||||
}
|
||||
ConfigurationsList {
|
||||
margin-top: 12px;
|
||||
}
|
||||
ErrorMessage {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -85,20 +94,35 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
|
||||
controller.setAdminMode(administrationScreenService.activeScreen !== null);
|
||||
}
|
||||
|
||||
const showTabs = !provider && controller.providers.length > 1;
|
||||
const dialogTitle = `${controller.provider?.label || ''} ${translate('authentication_login_dialog_title')}`;
|
||||
|
||||
const additional = userInfo.data !== null
|
||||
&& controller.provider?.id !== undefined
|
||||
&& !userInfo.hasToken(controller.provider.id);
|
||||
|
||||
const providers = useMemo(() => computed(() =>
|
||||
controller.providers.filter(provider => !provider.configurable || !!provider.configurations?.length)
|
||||
), [controller.providers]).get();
|
||||
|
||||
const showTabs = !provider && providers.length > 1;
|
||||
const configurable = !!controller.provider?.configurable;
|
||||
|
||||
const dialogTitle = `${controller.provider?.label || ''} ${translate('authentication_login_dialog_title')}`;
|
||||
let subTitle: string | undefined;
|
||||
|
||||
if (configurable) {
|
||||
subTitle = translate('authentication_identity_provider_dialog_subtitle');
|
||||
}
|
||||
|
||||
if (additional) {
|
||||
subTitle = translate('authentication_request_token');
|
||||
}
|
||||
|
||||
return styled(useStyles(styles, UNDERLINE_TAB_STYLES))(
|
||||
<TabsState currentTabId={controller.provider?.id}>
|
||||
<CommonDialogWrapper
|
||||
title={dialogTitle}
|
||||
icon={controller.provider?.icon}
|
||||
subTitle={additional ? translate('authentication_request_token') : undefined}
|
||||
footer={(
|
||||
subTitle={subTitle}
|
||||
footer={!configurable && (
|
||||
<AuthDialogFooter
|
||||
isAuthenticating={controller.isAuthenticating}
|
||||
onLogin={controller.login}
|
||||
@@ -116,7 +140,7 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
|
||||
>
|
||||
{showTabs && (
|
||||
<TabList aria-label='Auth providers'>
|
||||
{controller.providers.map(provider => (
|
||||
{providers.map(provider => (
|
||||
<Tab
|
||||
key={provider.id}
|
||||
tabId={provider.id}
|
||||
@@ -129,13 +153,20 @@ export const AuthDialog: DialogComponent<IAuthPayload, null> = observer(function
|
||||
))}
|
||||
</TabList>
|
||||
)}
|
||||
<SubmittingForm onSubmit={controller.login}>
|
||||
<SubmittingForm {...use({ form: !configurable })} onSubmit={controller.login}>
|
||||
{controller.provider && (
|
||||
<AuthProviderForm
|
||||
provider={controller.provider}
|
||||
credentials={controller.credentials}
|
||||
authenticate={controller.isAuthenticating}
|
||||
/>
|
||||
configurable ? (
|
||||
<ConfigurationsList
|
||||
configurations={controller.provider.configurations!}
|
||||
providerIcon={controller.provider.icon}
|
||||
/>
|
||||
) : (
|
||||
<AuthProviderForm
|
||||
provider={controller.provider}
|
||||
credentials={controller.credentials}
|
||||
authenticate={controller.isAuthenticating}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{controller.isLoading && <Loader />}
|
||||
{!controller.isLoading && !controller.provider && <>Select available provider</>}
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2021 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 { computed } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useState, useMemo } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Filter, IconOrImage, Link, Cell } from '@cloudbeaver/core-blocks';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import type { AuthProviderConfiguration } from '@cloudbeaver/core-sdk';
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
interface Props {
|
||||
configurations: AuthProviderConfiguration[];
|
||||
providerIcon?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
Cell {
|
||||
composes: theme-border-color-secondary from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
Filter {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
list {
|
||||
overflow: auto;
|
||||
}
|
||||
Cell {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
IconOrImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`);
|
||||
|
||||
export const ConfigurationsList: React.FC<Props> = observer(function ConfigurationsList({ configurations, providerIcon, className }) {
|
||||
const translate = useTranslate();
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const filteredConfigurations = useMemo(() => computed(() => {
|
||||
if (!search) {
|
||||
return configurations;
|
||||
}
|
||||
|
||||
return configurations.filter(configuration => {
|
||||
const target = `${configuration.displayName}${configuration.description || ''}`;
|
||||
return target.toUpperCase().includes(search.toUpperCase());
|
||||
});
|
||||
}), [search, configurations]);
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<container className={className}>
|
||||
{configurations.length >= 10 && (
|
||||
<Filter
|
||||
placeholder={translate('authentication_identity_provider_search_placeholder')}
|
||||
value={search}
|
||||
max
|
||||
onFilter={setSearch}
|
||||
/>
|
||||
)}
|
||||
<list>
|
||||
{filteredConfigurations.get().map(configuration => {
|
||||
const icon = configuration.iconURL || providerIcon;
|
||||
const title = `${configuration.displayName}\n${configuration.description || ''}`;
|
||||
return (
|
||||
<Link key={configuration.id} href={configuration.signInLink} title={title} wrapper>
|
||||
<Cell
|
||||
before={icon ? <IconOrImage icon={icon} /> : undefined}
|
||||
description={configuration.description}
|
||||
>
|
||||
{configuration.displayName}
|
||||
</Cell>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</list>
|
||||
</container>
|
||||
);
|
||||
});
|
||||
@@ -4,6 +4,8 @@ export default [
|
||||
['authentication_logout', 'Logout'],
|
||||
['authentication_authenticate', 'Authenticate'],
|
||||
['authentication_request_token', 'Additional authentication is required'],
|
||||
['authentication_identity_provider_search_placeholder', 'Search for configuration name or description...'],
|
||||
['authentication_identity_provider_dialog_subtitle', 'Choose configuration you want to sign in with'],
|
||||
['authentication_administration_user_connections_user_add', 'User Creation'],
|
||||
['authentication_administration_user_connections_user_new', 'New user'],
|
||||
['authentication_administration_user_connections_access_load_fail', 'User\'s granted connections loading failed'],
|
||||
|
||||
@@ -2,6 +2,8 @@ export default [
|
||||
['authentication_login_dialog_title', 'Авторизация'],
|
||||
['authentication_login', 'Войти'],
|
||||
['authentication_logout', 'Выйти'],
|
||||
['authentication_identity_provider_search_placeholder', 'Поиск по имени или описанию конфигурации...'],
|
||||
['authentication_identity_provider_dialog_subtitle', 'Выберите конфигурацию с помощью которой вы хотите войти'],
|
||||
['authentication_administration_user_connections_user_add', 'Создание пользователя'],
|
||||
['authentication_administration_user_connections_user_new', 'Новый пользователь'],
|
||||
['authentication_administration_user_connections_access_load_fail', 'Не удалось загрузить доступные пользователю коннекшены'],
|
||||
|
||||
Reference in New Issue
Block a user