Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions src/containers/App/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ import React from 'react';

import {connect} from 'react-redux';
import type {RedirectProps} from 'react-router-dom';
import {Redirect, Route, Switch} from 'react-router-dom';
import {Redirect, Route, Switch, useLocation} from 'react-router-dom';

import {AccessDenied} from '../../components/Errors/403';
import {PageError} from '../../components/Errors/PageError/PageError';
import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper';
import {useSlots} from '../../components/slots';
import type {SlotMap} from '../../components/slots/SlotMap';
import type {SlotComponent} from '../../components/slots/types';
import routes from '../../routes';
import routes, {checkIsClustersPage} from '../../routes';
import type {RootState} from '../../store';
import {authenticationApi} from '../../store/reducers/authentication/authentication';
import {
useCapabilitiesQuery,
useClusterWithoutAuthInUI,
useMetaCapabilitiesLoaded,
useMetaCapabilitiesQuery,
useMetaLoginAvailable,
} from '../../store/reducers/capabilities/hooks';
import {nodesListApi} from '../../store/reducers/nodesList';
import {uiFactory} from '../../uiFactory/uiFactory';
Expand Down Expand Up @@ -160,7 +161,7 @@ export function Content(props: ContentProps) {
exact: true,
component: Clusters,
slot: ClustersSlot,
wrapper: GetMetaCapabilities,
wrapper: ClustersDataWrapper,
})}
{/* Single cluster routes */}
{routesSlots.map((route) => {
Expand Down Expand Up @@ -191,9 +192,34 @@ function DataWrapper({children}: {children: React.ReactNode}) {
);
}

function GetUser({children}: {children: React.ReactNode}) {
function ClustersDataWrapper({children}: {children: React.ReactNode}) {
return (
<GetMetaCapabilities>
<GetMetaUser>{children}</GetMetaUser>
</GetMetaCapabilities>
);
}

function GetMetaUser({children}: {children: React.ReactNode}) {
const location = useLocation();

const isClustersPage = checkIsClustersPage(location.pathname);

const isMetaLoginAvailable = useMetaLoginAvailable();

if (isClustersPage && isMetaLoginAvailable) {
return <GetUser useMeta>{children}</GetUser>;
}
return children;
}

function GetUser({children, useMeta}: {children: React.ReactNode; useMeta?: boolean}) {
const database = useDatabaseFromQuery();
const {isLoading, error} = authenticationApi.useWhoamiQuery({database});

const {isLoading, error} = authenticationApi.useWhoamiQuery({
database,
useMeta,
});
const {appTitle} = useAppTitle();

const errorProps = error ? {...uiFactory.clusterOrDatabaseAccessError} : undefined;
Expand Down Expand Up @@ -262,8 +288,15 @@ function ContentWrapper(props: ContentWrapperProps) {
const {singleClusterMode, isAuthenticated} = props;
const authUnavailable = useClusterWithoutAuthInUI();

const location = useLocation();
const isClustersPage = checkIsClustersPage(location.pathname);

const isMetaLoginAvailable = useMetaLoginAvailable();

const isClustersAuthUnavailable = isClustersPage && !isMetaLoginAvailable;

const renderNotAuthenticated = () => {
if (authUnavailable) {
if (authUnavailable || isClustersAuthUnavailable) {
return <AccessDenied />;
}
return <Authentication />;
Expand Down
13 changes: 9 additions & 4 deletions src/containers/Authentication/Authentication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {Eye, EyeSlash, Xmark} from '@gravity-ui/icons';
import {Button, Link as ExternalLink, Icon, TextInput} from '@gravity-ui/uikit';
import {useHistory, useLocation} from 'react-router-dom';

import {parseQuery} from '../../routes';
import {checkIsClustersPage, parseQuery} from '../../routes';
import {authenticationApi} from '../../store/reducers/authentication/authentication';
import {useLoginWithDatabase} from '../../store/reducers/capabilities/hooks';
import {useLoginWithDatabase, useMetaLoginAvailable} from '../../store/reducers/capabilities/hooks';
import {cn} from '../../utils/cn';

import {isDatabaseError, isPasswordError, isUserError} from './utils';
Expand All @@ -27,7 +27,10 @@ function Authentication({closable = false}: AuthenticationProps) {

const needDatabase = useLoginWithDatabase();

const [authenticate, {isLoading}] = authenticationApi.useAuthenticateMutation(undefined);
const isClustersPage = checkIsClustersPage(location.pathname);
const isMetaLoginAvailable = useMetaLoginAvailable();

const [authenticate, {isLoading}] = authenticationApi.useAuthenticateMutation();
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The undefined parameter was removed but the function call was changed from useAuthenticateMutation(undefined) to useAuthenticateMutation(). While functionally equivalent, the explicit change in the diff suggests this may have been intentional. Verify this change is necessary and not just a formatting modification.

Copilot uses AI. Check for mistakes.

const {returnUrl, database: databaseFromQuery} = parseQuery(location);

Expand All @@ -53,8 +56,10 @@ function Authentication({closable = false}: AuthenticationProps) {
setPasswordError('');
};

const useMeta = isClustersPage && isMetaLoginAvailable;

const onLoginClick = () => {
authenticate({user: login, password, database})
authenticate({user: login, password, database, useMeta})
.unwrap()
.then(() => {
if (returnUrl) {
Expand Down
5 changes: 3 additions & 2 deletions src/containers/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {useHistory, useLocation} from 'react-router-dom';

import {getConnectToDBDialog} from '../../components/ConnectToDB/ConnectToDBDialog';
import {InternalLink} from '../../components/InternalLink';
import {checkIsClustersPage, checkIsTenantPage} from '../../routes';
import {
useAddClusterFeatureAvailable,
useDatabasesAvailable,
Expand Down Expand Up @@ -64,8 +65,8 @@ function Header() {
const location = useLocation();
const history = useHistory();

const isDatabasePage = location.pathname === '/tenant';
const isClustersPage = location.pathname === '/clusters';
const isDatabasePage = checkIsTenantPage(location.pathname);
const isClustersPage = checkIsClustersPage(location.pathname);

const {isLoading: isClustersLoading, error: clustersError} =
clustersApi.useGetClustersListQuery(undefined, {
Expand Down
8 changes: 8 additions & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,11 @@ type TabletPageQuery = QueryParamsTypeFromQueryObject<typeof tabletPageQueryPara
export function getTabletPagePath(tabletId: string | number, query: TabletPageQuery = {}) {
return createHref(routes.tablet, {id: tabletId}, {...query});
}

export function checkIsClustersPage(pathname: string) {
return pathname.endsWith(routes.clusters);
}

export function checkIsTenantPage(pathname: string) {
return pathname.endsWith(routes.tenant);
Comment on lines +151 to +155
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using endsWith() for route matching is fragile and will produce false positives. For example, /some/other/clusters would match even though it's not the clusters page. Use exact matching or a more robust pattern matching approach: pathname === routes.clusters or use a library like matchPath from react-router.

Suggested change
return pathname.endsWith(routes.clusters);
}
export function checkIsTenantPage(pathname: string) {
return pathname.endsWith(routes.tenant);
return pathname === routes.clusters;
}
export function checkIsTenantPage(pathname: string) {
return pathname === routes.tenant;

Copilot uses AI. Check for mistakes.
}
9 changes: 9 additions & 0 deletions src/services/api/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
MetaClusters,
MetaTenants,
} from '../../types/api/meta';
import type {TUserToken} from '../../types/api/whoami';
import {parseMetaTenants} from '../parsers/parseMetaTenants';

import type {AxiosOptions, BaseAPIParams} from './base';
Expand All @@ -28,6 +29,14 @@ export class MetaAPI extends BaseYdbAPI {
return `${META_BACKEND ?? ''}${path}`;
}

metaAuthenticate(params: {user: string; password: string}) {
return this.post(this.getPath('/meta/login'), params, {});
Comment on lines +32 to +33
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metaAuthenticate method lacks a return type annotation. Based on the usage in authentication.ts where the result is dispatched, add an explicit return type such as Promise<TUserToken> or the appropriate type for the authentication response.

Suggested change
metaAuthenticate(params: {user: string; password: string}) {
return this.post(this.getPath('/meta/login'), params, {});
metaAuthenticate(params: {user: string; password: string}): Promise<TUserToken> {
return this.post<TUserToken>(this.getPath('/meta/login'), params, {});

Copilot uses AI. Check for mistakes.
}

metaWhoami() {
return this.post<TUserToken>(this.getPath('/meta/whoami'), {}, {});
}

getMetaCapabilities() {
return this.get<MetaCapabilitiesResponse>(
this.getPath('/capabilities'),
Expand Down
22 changes: 18 additions & 4 deletions src/store/reducers/authentication/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,17 @@ export const {selectIsUserAllowedToMakeChanges, selectIsViewerUser, selectUser}
export const authenticationApi = api.injectEndpoints({
endpoints: (build) => ({
whoami: build.query({
queryFn: async ({database}: {database?: string}, {dispatch}) => {
queryFn: async (
{database, useMeta}: {database?: string; useMeta?: boolean},
{dispatch},
) => {
try {
const data = await window.api.viewer.whoami({database});
let data: TUserToken;
if (useMeta && window.api.meta) {
data = await window.api.meta.metaWhoami();
} else {
data = await window.api.viewer.whoami({database});
}
dispatch(setUser(data));
return {data};
} catch (error) {
Expand All @@ -68,11 +76,17 @@ export const authenticationApi = api.injectEndpoints({
}),
authenticate: build.mutation({
queryFn: async (
params: {user: string; password: string; database?: string},
params: {user: string; password: string; database?: string; useMeta?: boolean},
{dispatch},
) => {
try {
const data = await window.api.auth.authenticate(params);
const {useMeta, ...rest} = params;
let data;
if (useMeta) {
data = await window.api.meta?.metaAuthenticate(rest);
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional chaining on window.api.meta?.metaAuthenticate could result in data being undefined if window.api.meta is null/undefined. This would cause the subsequent dispatch(setIsAuthenticated(true)) to execute even when authentication fails. Add a null check or use non-null assertion if the condition guarantees window.api.meta exists.

Suggested change
data = await window.api.meta?.metaAuthenticate(rest);
if (!window.api.meta) {
throw new Error('Meta API is not available for authentication');
}
data = await window.api.meta.metaAuthenticate(rest);

Copilot uses AI. Check for mistakes.
} else {
data = await window.api.auth.authenticate(rest);
}
dispatch(setIsAuthenticated(true));
return {data};
} catch (error) {
Expand Down
4 changes: 4 additions & 0 deletions src/store/reducers/capabilities/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,7 @@ export const useClusterEventsAvailable = () => {
export const useDatabasesAvailable = () => {
return useGetMetaFeatureVersion('/meta/databases') >= 1;
};

export const useMetaLoginAvailable = () => {
return useGetMetaFeatureVersion('/meta/login') >= 1;
};
1 change: 1 addition & 0 deletions src/types/api/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ export type MetaCapability =
| '/meta/update_cluster'
| '/meta/delete_cluster'
| '/meta/events'
| '/meta/login'
| '/meta/databases';
Loading