Skip to main content
Glama
index.ts14.6 kB
import {create} from 'zustand' import {Schema} from 'amis' import ajax from '@/utils/ajax' import {getCurrentClusterId, setCurrentClusterId} from '@/utils/utils' interface Store { schema: Schema loading: boolean initPage: (path: string) => void } const useStore = create<Store>((set) => ({ schema: { type: 'page' }, loading: false, initPage(path) { set({loading: true}) if (path.startsWith('/crd/namespaced_cr') || path.startsWith('/crd/cluster_cr')) { try { const hash = window.location.hash || ''; const queryString = hash.includes('?') ? hash.split('?')[1] : ''; const urlParams = new URLSearchParams(queryString); const group = urlParams.get('group') || ''; const kind = urlParams.get('kind') || ''; const version = urlParams.get('version') || ''; const scope = urlParams.get('scope') || (path.startsWith('/crd/namespaced_cr') ? 'Namespaced' : 'Cluster'); const cols = urlParams.get('cols') || ''; const mode = urlParams.get('mode') || 'append'; const ns = urlParams.get('ns') || ''; const linkCluster = urlParams.get('cluster') || ''; // 如果链接中携带 cluster,则切换并刷新 if (linkCluster) { const originCluster = getCurrentClusterId(); if (originCluster !== linkCluster) { setCurrentClusterId(linkCluster); // 刷新后会重新构建页面 window.location.reload(); return; } } const defaultNs = ns || (localStorage.getItem('selectedNs') || (scope === 'Namespaced' ? 'default' : '')); if (!group || !kind || !version) { set({ schema: { type: 'page', body: [{ type: 'alert', level: 'danger', body: '缺少必要参数:group、kind、version' }] } }); return; } // 构建本地schema const pageSchema: Schema = buildDynamicCRDSchema({ group, kind, version, scope, ns: defaultNs, cols, mode }) as unknown as Schema; set({ schema: pageSchema }); } catch (error: any) { console.error('Failed to build CRD page schema:', error); set({ schema: { type: 'page', body: [{ type: 'alert', level: 'danger', body: `构建CRD页面失败: ${error?.message || '未知错误'}` }] } }); } finally { set({loading: false}) } } else { let page = path.slice(1); page = page + '.json'; const url = `/public/pages/${page}`; ajax.get(url).then(res => { set({ schema: res.data }) }).finally(() => { set({loading: false}) }) } } })) export default useStore function safeParseJSON<T = any>(input: string): T | null { try { return JSON.parse(input) as T; } catch { return null; } } function tryDecodeBase64JSON<T = any>(input: string): T | null { try { const normalized = input.replace(/\s/g, ''); const base64 = normalized.replace(/-/g, '+').replace(/_/g, '/'); const padLen = (4 - (base64.length % 4)) % 4; const padded = base64 + '='.repeat(padLen); const decoded = atob(padded); return JSON.parse(decoded) as T; } catch { return null; } } function parseCustomColumns(cols: string | null | undefined): any[] { if (!cols) return []; // 先尝试直接解析 JSON const asJson = safeParseJSON<any[]>(cols); if (Array.isArray(asJson)) return asJson; // 尝试 Base64 解码后解析 const asB64 = tryDecodeBase64JSON<any[]>(cols); if (Array.isArray(asB64)) return asB64; // 兜底:返回空 return []; } function buildDynamicCRDSchema(props: { group: string; kind: string; version: string; scope: string; // Namespaced | Cluster ns: string; // 逗号分隔可多选 cols?: string; mode?: string; // append | replace }): any { const { group, kind, version, scope, ns, cols, mode } = props; const customColumns = parseCustomColumns(cols); const replaceMode = mode === 'replace'; const defaultColumns: any[] = []; // 操作列始终放在最前 const operationColumn: any = { type: 'operation', label: '操作', width: 120, buttons: [ { type: 'button', icon: 'fas fa-eye text-primary', actionType: 'drawer', tooltip: '资源描述', drawer: { closeOnEsc: true, closeOnOutside: true, size: 'xl', title: 'Describe: ${metadata.name} (ESC 关闭)', body: [ { type: 'service', api: `post:/k8s/${kind}/group/${group}/version/${version}/describe/ns/$metadata.namespace/name/$metadata.name`, body: [ { type: 'highlightHtml', keywords: ['Error', 'Warning'], html: '${result}' } ] } ] } }, { type: 'button', icon: 'fa fa-edit text-primary', tooltip: 'Yaml编辑', actionType: 'drawer', drawer: { closeOnEsc: true, closeOnOutside: true, size: 'lg', title: 'Yaml管理', body: [ { type: 'tabs', tabsMode: 'tiled', tabs: [ { title: '查看编辑', body: [ { type: 'service', api: `get:/k8s/${kind}/group/${group}/version/${version}/ns/$metadata.namespace/name/$metadata.name`, body: [ { type: 'mEditor', text: '${yaml}', componentId: 'yaml', saveApi: `/k8s/${kind}/group/${group}/version/${version}/update/ns/${'${metadata.namespace}'}/name/${'${metadata.name}'}`, options: { language: 'yaml', wordWrap: 'on', scrollbar: { vertical: 'auto' } } } ] } ] } ] } ] } }, { type: 'button', icon: 'fa fa-trash text-danger', label: '', confirmText: '确定要删除该资源?', actionType: 'ajax', api: `post:/k8s/${kind}/group/${group}/version/${version}/remove/ns/$metadata.namespace/name/$metadata.name` } ] }; // 默认列 defaultColumns.push({ name: 'metadata.name', label: '名称', type: 'text', sortable: true, width: '220px' }); if (scope === 'Namespaced') { defaultColumns.push({ name: 'metadata.namespace', label: '命名空间', type: 'text', width: '160px' }); } defaultColumns.push({ name: 'metadata.creationTimestamp', label: '存在时长', type: 'k8sAge' }); const mergedColumns = replaceMode ? customColumns : [...defaultColumns, ...customColumns]; // 批量动作 const bulkActions = [ { label: '批量删除', actionType: 'ajax', confirmText: '确定要批量删除?', api: { url: `/k8s/${kind}/group/${group}/version/${version}/batch/remove`, method: 'post', data: { name_list: "${selectedItems | pick:metadata.name }", ns_list: "${selectedItems | pick:metadata.namespace }" } } }, { label: '强制删除', actionType: 'ajax', confirmText: '确定要批量强制删除?', api: { url: `/k8s/${kind}/group/${group}/version/${version}/force_remove`, method: 'post', data: { name_list: "${selectedItems | pick:metadata.name }", ns_list: "${selectedItems | pick:metadata.namespace }" } } } ]; // API 构建,统一走 list/ns/$ns(后端已注册空 ns 的路由) const listUrl = `/k8s/${kind}/group/${group}/version/${version}/list/ns/$ns`; return { type: 'page', data: { kind, group, version, scope, // 初始 ns:URL 提供优先,否则沿用本地选择 ns: ns || "${'default'|selectedNs}" }, body: [ { type: 'container', className: 'floating-toolbar-right', body: [ { type: 'wrapper', style: { display: 'inline-flex' }, body: [ { type: 'form', mode: 'inline', wrapWithPanel: false, body: [ { label: '集群', type: 'select', multiple: false, name: 'cluster', id: 'cluster', searchable: true, source: '/params/cluster/option_list', value: "${''|selectedCluster}", onEvent: { change: { actions: [ { actionType: 'custom', script: "window.setCurrentClusterId(event.data.value)" }, { actionType: 'custom', script: 'window.location.reload();' } ] } } }, ...(scope === 'Namespaced' ? [ { label: '命名空间', type: 'select', name: 'ns', searchable: true, clearable: true, multiple: true, maxTagCount: 1, source: '/k8s/ns/option_list', onEvent: { change: { actions: [ { actionType: 'custom', script: "const v = Array.isArray(event.data.value)?event.data.value.join(','):String(event.data.value||''); doAction('listCRUD','setValue',{ns:v}); localStorage.setItem('selectedNs', v); doAction('listCRUD','reload');" } ] } } } ] : []) ] } ] } ] }, { type: 'crud', id: 'listCRUD', name: 'listCRUD', autoFillHeight: true, autoGenerateFilter: { columnsNum: 4, showBtnToolbar: false, defaultCollapsed: false }, headerToolbar: [ { type: 'columns-toggler', align: 'right', draggable: true, icon: 'fas fa-cog', overlay: true, footerBtnSize: 'sm' }, { type: 'tpl', tpl: '共${count}条', align: 'right', visibleOn: '${count}' }, 'reload', 'bulkActions' ], loadDataOnce: false, syncLocation: false, perPage: 10, bulkActions, api: { url: listUrl, method: 'post' }, columns: [operationColumn, ...mergedColumns] } ] }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/weibaohui/k8m'

If you have feedback or need assistance with the MCP directory API, please join our Discord server