diff --git a/packages/ui/src/@types/core.d.ts b/packages/ui/src/@types/core.d.ts index cb8132af3..ae9fc276f 100644 --- a/packages/ui/src/@types/core.d.ts +++ b/packages/ui/src/@types/core.d.ts @@ -57,7 +57,7 @@ export interface YTCoreConfig { * The OAuthRobot should have read/write access to mapNodePath */ userSettingsConfig?: { - cluster: string; + cluster?: string; // path to a map node with user-settings files mapNodePath: string; }; @@ -70,7 +70,7 @@ export interface YTCoreConfig { * The OAuthRobot should have read/write access to the table. */ userColumnPresets?: { - cluster: string; + cluster?: string; dynamicTablePath: string; }; diff --git a/packages/ui/src/server/components/layout-config.ts b/packages/ui/src/server/components/layout-config.ts index 9d32d2e79..d88da65c0 100644 --- a/packages/ui/src/server/components/layout-config.ts +++ b/packages/ui/src/server/components/layout-config.ts @@ -17,8 +17,14 @@ interface Params { export async function getLayoutConfig(req: Request, params: Params): Promise { const {login, ytConfig, settings} = params; - const {ytApiUseCORS, uiSettings, metrikaCounter, allowPasswordAuth, odinBaseUrl} = req.ctx - .config as YTCoreConfig; + const { + ytApiUseCORS, + uiSettings, + metrikaCounter, + allowPasswordAuth, + odinBaseUrl, + userSettingsConfig, + } = req.ctx.config as YTCoreConfig; const YT = ytConfig; const uiVersion = getInterfaceVersion(); @@ -49,6 +55,7 @@ export async function getLayoutConfig(req: Request, params: Params): Promise['userSettingsConfig']> { +export function getSettingsConfig(cluster: string): { + cluster: string; + mapNodePath?: string; +} { const {userSettingsConfig} = getApp().config; - return userSettingsConfig || {}; + + return { + cluster, + ...userSettingsConfig, + }; } -function getSettingsSetup() { - return getRobotYTApiSetup(getSettingsConfig().cluster!).setup; +function getSettingsSetup(cluster: string) { + return getRobotYTApiSetup(getSettingsConfig(cluster).cluster).setup; } export function isRemoteSettingsConfigured() { @@ -26,18 +32,19 @@ export function isRemoteSettingsConfigured() { interface Params { ctx: AppContext; username: string; + cluster: string; } -export function create({ctx, username}: Params) { +export function create({ctx, username, cluster}: Params) { ctx.log('settings.create', {username}); if (!isRemoteSettingsConfigured()) { return Promise.reject(makeConfigError()); } return yt.v3.create({ - setup: getSettingsSetup(), + setup: getSettingsSetup(cluster), parameters: { - path: getSettingsConfig().mapNodePath + '/' + username, + path: getSettingsConfig(cluster).mapNodePath + '/' + username, type: 'document', ignore_existing: true, // No error if exists attributes: { @@ -47,16 +54,16 @@ export function create({ctx, username}: Params) { }); } -export function get({ctx, username}: Params) { - ctx.log('settings.get', {username}); +export function get({ctx, username, cluster}: Params) { + ctx.log('settings.get', {username, cluster}); if (!isRemoteSettingsConfigured()) { return Promise.reject(makeConfigError()); } return yt.v3.get({ - setup: getSettingsSetup(), + setup: getSettingsSetup(cluster), parameters: { - path: getSettingsConfig().mapNodePath + '/' + username, + path: getSettingsConfig(cluster).mapNodePath + '/' + username, output_format: { $value: 'json', $attributes: { @@ -70,16 +77,16 @@ export function get({ctx, username}: Params) { interface GetParams extends Params { path: string; } -export function getItem({ctx, username, path}: GetParams) { +export function getItem({ctx, username, path, cluster}: GetParams) { ctx.log('settings.getItem', {username, path}); if (!isRemoteSettingsConfigured()) { return Promise.reject(makeConfigError()); } return yt.v3.get({ - setup: getSettingsSetup(), + setup: getSettingsSetup(cluster), parameters: { - path: getSettingsConfig().mapNodePath + '/' + username + '/' + path, + path: getSettingsConfig(cluster).mapNodePath + '/' + username + '/' + path, output_format: { $value: 'json', $attributes: { @@ -93,16 +100,16 @@ export function getItem({ctx, username, path}: GetParams) { interface SetParams extends GetParams { value: any; } -export function setItem({ctx, username, path, value}: SetParams) { - ctx.log('settings.setItem', {username, path, value}); +export function setItem({ctx, username, path, value, cluster}: SetParams) { + ctx.log('settings.setItem', {username, path, value, cluster}); if (!isRemoteSettingsConfigured()) { return Promise.reject(makeConfigError()); } return yt.v3.set({ - setup: getSettingsSetup(), + setup: getSettingsSetup(cluster), parameters: { - path: getSettingsConfig().mapNodePath + '/' + username + '/' + path, + path: getSettingsConfig(cluster).mapNodePath + '/' + username + '/' + path, input_format: { $value: 'json', $attributes: { @@ -113,16 +120,16 @@ export function setItem({ctx, username, path, value}: SetParams) { data: value, }); } -export function deleteItem({ctx, username, path}: GetParams) { - ctx.log('settings.deleteItem', {username, path}); +export function deleteItem({ctx, username, path, cluster}: GetParams) { + ctx.log('settings.deleteItem', {username, path, cluster}); if (!isRemoteSettingsConfigured()) { return Promise.reject(makeConfigError()); } return yt.v3.remove({ - setup: getSettingsSetup(), + setup: getSettingsSetup(cluster), parameters: { - path: getSettingsConfig().mapNodePath + '/' + username + '/' + path, + path: getSettingsConfig(cluster).mapNodePath + '/' + username + '/' + path, }, }); } diff --git a/packages/ui/src/server/components/table.ts b/packages/ui/src/server/components/table.ts index 95368ce7f..aa83597a6 100644 --- a/packages/ui/src/server/components/table.ts +++ b/packages/ui/src/server/components/table.ts @@ -9,19 +9,20 @@ const yt = ytLib(); const SHA1_REGEXP = /^[0-9a-f]{40}$/; interface PresetConfig { - cluster: string; + cluster?: string; dynamicTablePath: string; } export async function getColumnPreset( - {cluster, dynamicTablePath}: PresetConfig, + {dynamicTablePath, cluster}: PresetConfig, hash: string, + ytAuthCluster: string, ): Promise | undefined> { if (!SHA1_REGEXP.test(hash)) { throw new Error('The hash parameter should be defined as a valid sha1-hash string'); } - const {setup: setupConfig} = getRobotYTApiSetup(cluster); + const {setup: setupConfig} = getRobotYTApiSetup(cluster || ytAuthCluster); const query = `* FROM [${dynamicTablePath}] WHERE hash="${hash}" LIMIT 1`; const result = await yt.v3.selectRows({ @@ -56,14 +57,15 @@ export async function getColumnPreset( } export async function saveColumnPreset( - {cluster, dynamicTablePath}: PresetConfig, + {dynamicTablePath, cluster}: PresetConfig, columns: unknown | Array, + ytAuthCluster: string, ): Promise { if (!Array.isArray(columns) || _.some(columns, (i) => 'string' !== typeof i)) { throw new Error('Request body should contain JSON-array of strings'); } - const {setup: setupConfig} = getRobotYTApiSetup(cluster); + const {setup: setupConfig} = getRobotYTApiSetup(cluster || ytAuthCluster); const hash = objectHash.sha1(columns); diff --git a/packages/ui/src/server/controllers/home.ts b/packages/ui/src/server/controllers/home.ts index 4ce8ec8b5..3c7e111b1 100644 --- a/packages/ui/src/server/controllers/home.ts +++ b/packages/ui/src/server/controllers/home.ts @@ -8,6 +8,7 @@ import ServerFactory, {getApp} from '../ServerFactory'; import {isLocalModeByEnvironment} from '../utils'; import {getDafaultUserSettings} from '../utils/default-settings'; import {ODIN_PAGE_ID} from '../../shared/constants'; +import {getSettingsConfig} from '../../server/components/settings'; function isRootPage(page: string) { const rootPages = [ @@ -68,10 +69,14 @@ export async function homeIndex(req: Request, res: Response) { }, }; - if (login && useRemoteSettings) { + const settingsConfig = getSettingsConfig(cluster!); + + if (login && useRemoteSettings && settingsConfig.cluster) { try { - await create({ctx, username: login}); - const userSettings = login ? await get({ctx, username: login}) : {}; + await create({ctx, username: login, cluster: settingsConfig.cluster}); + const userSettings = login + ? await get({ctx, username: login, cluster: settingsConfig.cluster}) + : {}; settings.data = {...settings.data, ...userSettings}; } catch (e) { const message = `Error in getting user settings for ${login}`; diff --git a/packages/ui/src/server/controllers/settings.ts b/packages/ui/src/server/controllers/settings.ts index e3abd8b2f..ddaef441b 100644 --- a/packages/ui/src/server/controllers/settings.ts +++ b/packages/ui/src/server/controllers/settings.ts @@ -9,51 +9,51 @@ function sendResponse(_req: Request, res: Response, data: object) { export function settingsCreate(req: Request, res: Response) { const { ctx, - params: {username}, + params: {username, ytAuthCluster}, } = req; - return create({ctx, username}) + return create({ctx, username, cluster: ytAuthCluster}) .then(sendResponse.bind(null, req, res)) .catch(sendError.bind(null, res)); } export function settingsGet(req: Request, res: Response) { const { ctx, - params: {username}, + params: {username, ytAuthCluster}, } = req; - return get({ctx, username}) + return get({ctx, username, cluster: ytAuthCluster}) .then(sendResponse.bind(null, req, res)) .catch(sendError.bind(null, res)); } export async function settingsGetItem(req: Request, res: Response) { const { ctx, - params: {username, path}, + params: {username, path, ytAuthCluster}, } = req; - return getItem({ctx, username, path}) + return getItem({ctx, username, path, cluster: ytAuthCluster}) .then(sendResponse.bind(null, req, res)) .catch(sendError.bind(null, res)); } export function settingsSetItem(req: Request, res: Response) { const { ctx, - params: {username, path}, + params: {username, path, ytAuthCluster}, body: {value} = {}, } = req; - return setItem({ctx, username, path, value}) + return setItem({ctx, username, path, value, cluster: ytAuthCluster}) .then(sendResponse.bind(null, req, res)) .catch(sendError.bind(null, res)); } export function settingsDeleteItem(req: Request, res: Response) { const { ctx, - params: {username, path}, + params: {username, path, ytAuthCluster}, } = req; - return deleteItem({ctx, username, path}) + return deleteItem({ctx, username, path, cluster: ytAuthCluster}) .then(sendResponse.bind(null, req, res)) .catch(sendError.bind(null, res)); } diff --git a/packages/ui/src/server/controllers/table-column-preset.ts b/packages/ui/src/server/controllers/table-column-preset.ts index d3a9d86e1..c1f4e2430 100644 --- a/packages/ui/src/server/controllers/table-column-preset.ts +++ b/packages/ui/src/server/controllers/table-column-preset.ts @@ -4,11 +4,11 @@ import {prepareErrorToSend} from '../utils'; export async function tableColumnPresetGet(req: Request, res: Response) { const { - params: {hash}, + params: {hash, ytAuthCluster}, } = req; try { const presetsConfig = checkEnabled(req); - const columns = await getColumnPreset(presetsConfig, hash); + const columns = await getColumnPreset(presetsConfig, hash, ytAuthCluster); if (!columns) { const err = new Error(`Cannot find column-preset by the hash '${hash}'`); req.ctx.logError('Failed to get preset of columns', err); @@ -24,9 +24,13 @@ export async function tableColumnPresetGet(req: Request, res: Response) { export async function tableColumnPresetSave(req: Request, res: Response) { const {body} = req; + const { + params: {ytAuthCluster}, + } = req; + try { const presetsConfig = checkEnabled(req); - const result = await saveColumnPreset(presetsConfig, body); + const result = await saveColumnPreset(presetsConfig, body, ytAuthCluster); res.status(200).send(result); } catch (err) { req.ctx.logError('Failed to save preset of columns', err); diff --git a/packages/ui/src/server/routes.ts b/packages/ui/src/server/routes.ts index eb277743f..d7c7d0380 100644 --- a/packages/ui/src/server/routes.ts +++ b/packages/ui/src/server/routes.ts @@ -58,16 +58,16 @@ const routes: AppRoutes = { 'POST /api/chyt/:ytAuthCluster/:action': {handler: chytProxyApi}, - 'GET /api/settings/:username': {handler: settingsGet}, - 'POST /api/settings/:username': {handler: settingsCreate}, - 'GET /api/settings/:username/:path': {handler: settingsGetItem}, - 'PUT /api/settings/:username/:path': {handler: settingsSetItem}, - 'DELETE /api/settings/:username/:path': {handler: settingsDeleteItem}, + 'GET /api/settings/:ytAuthCluster/:username': {handler: settingsGet}, + 'POST /api/settings/:ytAuthCluster/:username': {handler: settingsCreate}, + 'GET /api/settings/:ytAuthCluster/:username/:path': {handler: settingsGetItem}, + 'PUT /api/settings/:ytAuthCluster/:username/:path': {handler: settingsSetItem}, + 'DELETE /api/settings/:ytAuthCluster/:username/:path': {handler: settingsDeleteItem}, - 'GET /api/table-column-preset/:hash': { + 'GET /api/table-column-preset/:ytAuthCluster/:hash': { handler: tableColumnPresetGet, }, - 'POST /api/table-column-preset': {handler: tableColumnPresetSave}, + 'POST /api/table-column-preset/:ytAuthCluster': {handler: tableColumnPresetSave}, 'GET /:ytAuthCluster/': HOME_INDEX_TARGET, 'GET /:ytAuthCluster/maintenance': {handler: homeRedirect}, diff --git a/packages/ui/src/shared/yt-types.d.ts b/packages/ui/src/shared/yt-types.d.ts index 35abefd46..ccd8f2ebd 100644 --- a/packages/ui/src/shared/yt-types.d.ts +++ b/packages/ui/src/shared/yt-types.d.ts @@ -234,6 +234,7 @@ export interface SettingsConfig { } export interface ConfigData { + userSettingsCluster?: string; settings: SettingsConfig; ytApiUseCORS?: boolean; uiSettings?: UISettings; diff --git a/packages/ui/src/ui/common/utils/settings-remote-provider.ts b/packages/ui/src/ui/common/utils/settings-remote-provider.ts index c07ca0cbf..fe77370a6 100644 --- a/packages/ui/src/ui/common/utils/settings-remote-provider.ts +++ b/packages/ui/src/ui/common/utils/settings-remote-provider.ts @@ -6,15 +6,23 @@ import {PromiseOrValue} from '../../../@types/types'; type Method = 'get' | 'put' | 'post' | 'delete'; -function api(method: Method, username: string, path: string, data?: unknown) { +interface ApiOptions { + method: Method; + username: string; + path: string; + data?: unknown; + cluster: string; +} + +function api(options: ApiOptions) { return axios .request({ - method: method, - url: '/api/settings/' + username + path, + method: options.method, + url: `/api/settings/${options.cluster}/${options.username}${options.path}`, headers: { 'content-type': 'application/json', }, - data: JSON.stringify(data), + data: JSON.stringify(options.data), }) .then((response) => { return response.data; @@ -37,37 +45,40 @@ function api(method: Method, username: string, path: string, data?: unknown) } const provider: SettingsProvider = { - get(username: string, path: string) { - return api('get', username, '/' + path); + get(username: string, path: string, cluster: string) { + return api({method: 'get', username, path: '/' + path, cluster}); }, - set(username: string, path: string, value: T) { + set(username: string, path: string, value: T, cluster: string) { if (value === undefined) { /** * data-ui/core uses body-parser which interprets empty body of request as empty object {}, * i.e. setItem from src/server/controllers/settings.js will receive req.body === {} if value === undefined, * so we have to remove it explicitly */ - return provider.remove(username, path); + return provider.remove(username, path, cluster); } - return api('put', username, '/' + path, {value}); + return api({method: 'put', username, path: '/' + path, data: {value}, cluster}); }, - remove(username: string, path: string) { - return api('delete', username, '/' + path); + remove(username: string, path: string, cluster: string) { + return api({method: 'delete', username, path: '/' + path, cluster}); }, - getAll(username: string) { - return api('get', username, '/'); + getAll(username: string, cluster: string) { + return api({method: 'get', username, path: '/', cluster}); }, - create(username: string) { - return api('post', username, '/'); + create(username: string, cluster: string) { + return api({method: 'post', username, path: '/', cluster}); }, }; export interface SettingsProvider { - get(username: string, path: string): Promise; - set(username: string, path: string, value: T): Promise; - remove(username: string, path: string): Promise; - getAll(username: string): PromiseOrValue; - create(username: string): Promise; + get(username: string, path: string, cluster: string): Promise; + set(username: string, path: string, value: T, cluster: string): Promise; + remove(username: string, path: string, cluster: string): Promise; + getAll( + username: string, + cluster: string, + ): PromiseOrValue; + create(username: string, cluster: string): Promise; } export default provider; diff --git a/packages/ui/src/ui/config/ui-settings.ts b/packages/ui/src/ui/config/ui-settings.ts index b4d685faa..27bbdfa4f 100644 --- a/packages/ui/src/ui/config/ui-settings.ts +++ b/packages/ui/src/ui/config/ui-settings.ts @@ -6,3 +6,6 @@ export function getConfigData(): ConfigData { export const uiSettings: Partial['uiSettings']> = getConfigData()?.uiSettings || {}; + +export const userSettingsCluster: ConfigData['userSettingsCluster'] = + getConfigData()?.userSettingsCluster; diff --git a/packages/ui/src/ui/containers/AppNavigation/AppNavigationComponent.tsx b/packages/ui/src/ui/containers/AppNavigation/AppNavigationComponent.tsx index e96ace41e..6614f05bc 100644 --- a/packages/ui/src/ui/containers/AppNavigation/AppNavigationComponent.tsx +++ b/packages/ui/src/ui/containers/AppNavigation/AppNavigationComponent.tsx @@ -13,9 +13,9 @@ import unknown from '../../../../img/user-avatar.svg'; import {AppNavigationProps} from './AppNavigationPageLayout'; import YT from '../../config/yt-config'; import UIFactory from '../../UIFactory'; +import {getSettingsCluster} from '../../store/selectors/global'; import './AppNavigationComponent.scss'; -import {getCluster} from '../../store/selectors/global'; const block = cn('yt-app-navigation'); @@ -53,15 +53,15 @@ function AppNavigationComponent({ }, [panelVisible, panelContent, settingsVisible, settingsContent]); const [popupVisible, setPopupVisible] = useState(false); - const cluster = useSelector(getCluster); + const settingsCluster = useSelector(getSettingsCluster); const history = useHistory(); let showUserIcon = Boolean(currentUser); - let showSettings = Boolean(currentUser); + let showSettings = settingsCluster && Boolean(currentUser); if (authWay === 'passwd') { - showUserIcon = Boolean(cluster) && Boolean(currentUser); - showSettings = Boolean(cluster) && Boolean(currentUser); + showUserIcon = Boolean(settingsCluster) && Boolean(currentUser); + showSettings = settingsCluster && Boolean(currentUser); } return ( diff --git a/packages/ui/src/ui/store/actions/settings/index.ts b/packages/ui/src/ui/store/actions/settings/index.ts index e04870382..38ef1ef81 100644 --- a/packages/ui/src/ui/store/actions/settings/index.ts +++ b/packages/ui/src/ui/store/actions/settings/index.ts @@ -10,6 +10,7 @@ import { UPDATE_SETTING_DATA, } from '../../../constants/index'; import {showToasterError, wrapApiPromiseByToaster} from '../../../utils/utils'; +import {getSettingsCluster} from '../../selectors/global'; function logError(action: string, name: string) { console.error('Failed to "%s" setting "%s", settings provider is disabled.', action, name); @@ -40,13 +41,14 @@ export function setSettingByKey(path: SettingKey, value: T): SettingsThunkAct global: {login}, } = getState(); const previousValue = data[path]; + const cluster = getSettingsCluster(getState()); dispatch({ type: SET_SETTING_VALUE, data: {path, value}, }); - return provider.set(login, path, value).catch((error) => { + return provider.set(login, path, value, cluster!).catch((error) => { if (error === 'disabled') { logError('set', path); return; @@ -73,13 +75,14 @@ export function removeSetting(settingName: string, settingNS: SettingNS): Settin global: {login}, } = getState(); const previousValue = data[path]; + const cluster = getSettingsCluster(getState()); dispatch({ type: UNSET_SETTING_VALUE, data: {path}, }); - return provider.remove(login, path).catch((error) => { + return provider.remove(login, path, cluster!).catch((error) => { if (error === 'disabled') { logError('remove', settingName); return; @@ -105,9 +108,10 @@ export function reloadSetting(settingName: string, settingNS: SettingNS): Settin settings: {provider}, global: {login}, } = getState(); + const cluster = getSettingsCluster(getState()); return provider - .get(login, path) + .get(login, path, cluster!) .then((value) => { dispatch({ type: SET_SETTING_VALUE, @@ -131,10 +135,12 @@ export function reloadUserSettings(login: string): SettingsThunkAction { return async (dispatch, getState) => { try { const state = getState(); + const cluster = getSettingsCluster(state); + const ytAuthCluster = state.global.ytAuthCluster || cluster; const {provider} = state.settings; + await provider.create(login, ytAuthCluster); + const allData = provider.getAll(login, ytAuthCluster); - await provider.create(login); - const allData = provider.getAll(login); const data: any = allData instanceof Promise ? await wrapApiPromiseByToaster(allData, { diff --git a/packages/ui/src/ui/store/reducers/settings.ts b/packages/ui/src/ui/store/reducers/settings.ts index 234ece356..7125db385 100644 --- a/packages/ui/src/ui/store/reducers/settings.ts +++ b/packages/ui/src/ui/store/reducers/settings.ts @@ -32,7 +32,7 @@ function getInitialState() { provider: useRemoteSettings ? remoteProvider : localProvider, // In case of disabled remote user settings we have to merge // default settings from server with settings from localStorage - data: useRemoteSettings ? data : {...data, ...localProvider.getAll(login)}, + data: useRemoteSettings ? data : {...data, ...localProvider.getAll(login, '')}, }; } diff --git a/packages/ui/src/ui/store/selectors/global/index.ts b/packages/ui/src/ui/store/selectors/global/index.ts index 18b6caacc..7bf8bb787 100644 --- a/packages/ui/src/ui/store/selectors/global/index.ts +++ b/packages/ui/src/ui/store/selectors/global/index.ts @@ -13,7 +13,7 @@ import {getClusterNS} from '../settings'; import {ClusterConfig} from '../../../../shared/yt-types'; import {FIX_MY_TYPE} from '../../../types'; import UIFactory from '../../../UIFactory'; -import {getConfigData} from '../../../config/ui-settings'; +import {getConfigData, userSettingsCluster} from '../../../config/ui-settings'; import {Page} from '../../../../shared/constants/settings'; import {AuthWay} from '../../../../shared/constants'; @@ -223,3 +223,7 @@ export const getOAuthButtonLabel = () => { export const getGlobalAsideHeaderWidth = (state: RootState) => state.global.asideHeaderWidth; export const getGlobalYTAuthCluster = (state: RootState) => state.global.ytAuthCluster; + +export const getSettingsCluster = createSelector([getCluster], (cluster) => { + return userSettingsCluster ?? cluster; +});