Skip to content
Open
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
5 changes: 5 additions & 0 deletions dashboard/fluid-ks-extension/extensions/fluid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# fluid

Fluid, elastic data abstraction and acceleration for BigData/AI applications in cloud.

> TODO: README
12 changes: 12 additions & 0 deletions dashboard/fluid-ks-extension/extensions/fluid/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "fluid",
"version": "1.0.0",
"private": true,
"description": "Fluid, elastic data abstraction and acceleration for BigData/AI applications in cloud.",
"homepage": "",
"author": "",
"main": "dist/index.js",
"files": [
"dist"
]
}
157 changes: 157 additions & 0 deletions dashboard/fluid-ks-extension/extensions/fluid/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React, { useMemo } from 'react';
import { Banner, Card } from '@kubed/components';
import { Book2Duotone, RocketDuotone, DownloadDuotone } from '@kubed/icons';
import { useNavigate, useLocation, useParams, Outlet } from 'react-router-dom';
import styled from 'styled-components';
import ClusterSelector from './components/ClusterSelector';
import fluidicon from './assets/fluidiconStr';


declare const t: (key: string, options?: any) => string;

// 由于 Layout 不在 @kubed/components 中,我们自己创建一个简单的 Layout
const Layout = styled.div`
display: flex;
width: 100%;
height: 100%;
`;

const Sider = styled.div`
flex: 0 0 220px;
width: 220px;
background: #fff;
box-shadow: 0 4px 8px rgba(36, 46, 66, 0.06);
z-index: 2;
overflow: auto;
`;

const Content = styled.div`
flex: 1;
overflow: auto;
`;

const FluidLayout = styled(Layout)`
height: 100vh;
`;

const LogoWrapper = styled.div`
height: 40px;
padding: 0 20px;
margin: 16px 0;
display: flex;
align-items: center;
gap: 8px;
`;

const ContentWrapper = styled.div`
padding: 24px;
background-color: #f5f7fa;
height: 100%;
overflow: auto;
`;

const StyledMenu = styled.div`
.menu-item {
display: flex;
align-items: center;
padding: 12px 20px;
margin: 4px 0;
cursor: pointer;
transition: all 0.3s ease;

.menu-icon {
margin-right: 10px;
}

&:hover {
background-color: #f5f7fa;
}

&.selected {
background-color: #f9fbfd;
border-right: 3px solid #00aa72;

.menu-icon {
color: #00aa72;
}
}
}
`;

const PageHeader = styled(Banner)`
margin-bottom: 20px;
`;

const menuItems = [
{
key: 'datasets',
icon: <Book2Duotone />,
label: 'DATASETS'
},
{
key: 'runtimes',
icon: <RocketDuotone />,
label: 'RUNTIMES'
},
{
key: 'dataloads',
icon: <DownloadDuotone/>,
label: 'DATALOADS'
}
];

export default function App() {
const navigate = useNavigate();
const location = useLocation();
const params = useParams<{ cluster: string }>();

// 从URL参数获取当前集群
const currentCluster = params.cluster || 'host';

const selectedKeys = useMemo(() => {
if (location.pathname.includes('/datasets')) {
return 'datasets';
}
if (location.pathname.includes('/runtimes')) {
return 'runtimes';
}
if (location.pathname.includes('/dataloads')) {
return 'dataloads';
}
return '';
}, [location]);

const handleMenuClick = (key: string) => {
const cluster = params.cluster || currentCluster || 'host';
navigate(`/fluid/${cluster}/${key}`);
};

return (
<FluidLayout>
<Sider>
<LogoWrapper>
<img src={`data:image/svg+xml;utf8,${encodeURIComponent(fluidicon)}`} alt="Fluid Logo" style={{ width: '120px', height: '40px' }} />
</LogoWrapper>
<ClusterSelector />
<StyledMenu>
{menuItems.map((item) => (
<div
key={item.key}
className={`menu-item ${selectedKeys === item.key ? 'selected' : ''}`}
onClick={() => handleMenuClick(item.key)}
>
<span className="menu-icon">{item.icon}</span>
<span>{t(item.label)}</span>
</div>
))}
</StyledMenu>
</Sider>
<Content>
<ContentWrapper>

<Outlet />
</ContentWrapper>
</Content>
</FluidLayout>
);
}
424 changes: 424 additions & 0 deletions dashboard/fluid-ks-extension/extensions/fluid/src/assets/fluidicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React, { useEffect, useState } from 'react';
import { Select } from '@kubed/components';
import { useNavigate, useLocation, useParams } from 'react-router-dom';
import styled from 'styled-components';

// 全局t函数声明
declare const t: (key: string, options?: any) => string;

// 集群信息类型
interface ClusterInfo {
name: string;
displayName: string;
}

const ClusterSelectorWrapper = styled.div`
padding: 12px 16px;
border-bottom: 1px solid #e3e9ef;
background: #f9fbfd;

.cluster-label {
font-size: 12px;
color: #79879c;
margin-bottom: 8px;
display: block;
}

.cluster-select {
width: 100%;
}
`;

const ClusterSelector: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const params = useParams<{ cluster: string }>();

// 本地状态管理
const [clusters, setClusters] = useState<ClusterInfo[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);

// 从URL参数获取当前集群
const currentCluster = params.cluster || 'host';

// 获取集群列表
const fetchClusters = async () => {
setIsLoading(true);
setError(null);
try {
// 获取集群列表不需要集群前缀,因为这是获取所有集群的API
const response = await fetch('/kapis/cluster.kubesphere.io/v1alpha1/clusters');

if (!response.ok) {
throw new Error(`Failed to fetch clusters: ${response.statusText}`);
}

const data = await response.json();
const clusterList: ClusterInfo[] = data.items?.map((item: any) => ({
name: item.metadata.name,
displayName: item.spec?.displayName || item.metadata.name
})) || [];

setClusters(clusterList);
} catch (error) {
console.error('获取集群列表失败:', error);
setError(error instanceof Error ? error.message : '获取集群列表失败');
} finally {
setIsLoading(false);
}
};

useEffect(() => {
fetchClusters();
}, []);

const handleClusterChange = (value: string) => {
console.log('切换集群:', value);

// 更新URL以反映新的集群选择
const pathParts = location.pathname.split('/');
if (pathParts.length >= 3 && pathParts[1] === 'fluid') {
// 新的URL结构分析:
// 列表页:/fluid/{cluster}/datasets -> ['', 'fluid', 'cluster', 'datasets']
// 详情页:/fluid/{cluster}/{namespace}/datasets/{name} -> ['', 'fluid', 'cluster', 'namespace', 'datasets', 'name']

if (pathParts.length >= 6) {
// 在详情页,重定向到对应的列表页
const resourceType = pathParts[4]; // datasets, runtimes, dataloads等
navigate(`/fluid/${value}/${resourceType}`);
} else if (pathParts.length === 4) {
// 在列表页,保持当前页面类型
const currentPage = pathParts[3] || 'datasets';
navigate(`/fluid/${value}/${currentPage}`);
} else {
// 其他情况,默认导航到datasets页面
navigate(`/fluid/${value}/datasets`);
}
} else {
// 默认导航到datasets页面
navigate(`/fluid/${value}/datasets`);
}

// 显示切换成功提示
// notify.success(t('CLUSTER_SWITCHED_SUCCESS', { cluster: value }));
};

if (error) {
console.error('集群选择器错误:', error);
// 如果获取集群列表失败,仍然显示当前集群
return (
<ClusterSelectorWrapper>
<span className="cluster-label">{t('CLUSTER')}</span>
<Select
className="cluster-select"
value={currentCluster}
disabled
options={[{
value: currentCluster,
label: currentCluster
}]}
/>
</ClusterSelectorWrapper>
);
}

return (
<ClusterSelectorWrapper>
<span className="cluster-label">{t('CLUSTER')}</span>
<Select
className="cluster-select"
value={currentCluster}
onChange={handleClusterChange}
loading={isLoading}
options={clusters.map(cluster => ({
value: cluster.name,
label: cluster.displayName
}))}
placeholder={isLoading ? t('LOADING') : t('SELECT_CLUSTER')}
/>
</ClusterSelectorWrapper>
);
};

export default ClusterSelector;
Loading
Loading