| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| import { useQuery } from '@tanstack/react-query'; | |
| import { legacyPostJsonMutation } from './operations'; | |
| export type ConfigurationScope = 'fe' | 'be'; | |
| export interface ConfigurationRow { | |
| key: string; | |
| name: string; | |
| node: string; | |
| nodeType: string; | |
| valueType: string; | |
| masterOnly: boolean | null; | |
| currentValue: string; | |
| mutable: boolean; | |
| } | |
| interface ConfigurationPayload { | |
| column_names?: unknown; | |
| columnNames?: unknown; | |
| rows?: unknown; | |
| } | |
| const aliases = { | |
| name: ['配置项', 'Name'], | |
| node: ['节点', 'Node'], | |
| nodeType: ['节点类型', 'Node Type'], | |
| valueType: ['配置值类型', 'Value Type'], | |
| masterOnly: ['MasterOnly', 'Master Only'], | |
| currentValue: ['配置值', 'Current Value', 'Value'], | |
| mutable: ['可修改', 'Mutable'], | |
| } as const; | |
| function columnIndex(columns: string[], names: readonly string[], fallback: number): number { | |
| const index = names.map((name) => columns.indexOf(name)).find((candidate) => candidate !== -1); | |
| return index ?? fallback; | |
| } | |
| function cell(row: unknown[], index: number): string { | |
| if (index < 0 || index >= row.length) return ''; | |
| const value = row[index]; | |
| if (value === null || value === undefined) return ''; | |
| if (typeof value === 'string') return value; | |
| if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') { | |
| return String(value); | |
| } | |
| return JSON.stringify(value); | |
| } | |
| function booleanValue(value: string): boolean { | |
| return /^(true|yes|1)$/i.test(value.trim()); | |
| } | |
| export function adaptConfiguration(payload: ConfigurationPayload, scope: ConfigurationScope): ConfigurationRow[] { | |
| const rawColumns = payload.column_names ?? payload.columnNames; | |
| const columns = Array.isArray(rawColumns) ? rawColumns.map(String) : []; | |
| const fallback = scope === 'fe' | |
| ? { name: 0, node: 1, nodeType: 2, valueType: 3, masterOnly: 4, currentValue: 5, mutable: 6 } | |
| : { name: 0, node: 1, nodeType: 2, valueType: 3, masterOnly: -1, currentValue: 4, mutable: 5 }; | |
| const indexes = { | |
| name: columnIndex(columns, aliases.name, fallback.name), | |
| node: columnIndex(columns, aliases.node, fallback.node), | |
| nodeType: columnIndex(columns, aliases.nodeType, fallback.nodeType), | |
| valueType: columnIndex(columns, aliases.valueType, fallback.valueType), | |
| masterOnly: columnIndex(columns, aliases.masterOnly, fallback.masterOnly), | |
| currentValue: columnIndex(columns, aliases.currentValue, fallback.currentValue), | |
| mutable: columnIndex(columns, aliases.mutable, fallback.mutable), | |
| }; | |
| const rows = Array.isArray(payload.rows) ? payload.rows : []; | |
| return rows.filter(Array.isArray).map((rawRow, rowIndex) => { | |
| const row = rawRow as unknown[]; | |
| const name = cell(row, indexes.name); | |
| const node = cell(row, indexes.node); | |
| return { | |
| key: `${scope}:${node}:${name}:${rowIndex}`, | |
| name, | |
| node, | |
| nodeType: cell(row, indexes.nodeType) || scope.toUpperCase(), | |
| valueType: cell(row, indexes.valueType), | |
| masterOnly: indexes.masterOnly < 0 ? null : booleanValue(cell(row, indexes.masterOnly)), | |
| currentValue: cell(row, indexes.currentValue), | |
| mutable: booleanValue(cell(row, indexes.mutable)), | |
| }; | |
| }); | |
| } | |
| /** | |
| * Reads configuration. The UI never writes it: changing a setting goes through | |
| * ADMIN SET FRONTEND CONFIG or the backend configuration API, both of which | |
| * authenticate the operator directly. | |
| * | |
| * This is a POST that carries the CSRF header because the FE endpoint is a POST. | |
| */ | |
| export async function fetchConfiguration(scope: ConfigurationScope): Promise<ConfigurationRow[]> { | |
| const payload = await legacyPostJsonMutation<ConfigurationPayload>( | |
| `/rest/v2/manager/node/configuration_info?type=${scope}`, | |
| {}, | |
| ); | |
| return adaptConfiguration(payload, scope); | |
| } | |
| export function useConfiguration(scope: ConfigurationScope) { | |
| return useQuery({ | |
| queryKey: ['configuration', scope], | |
| queryFn: () => fetchConfiguration(scope), | |
| refetchInterval: false, | |
| }); | |
| } |