mirror of
https://github.com/apache/superset.git
synced 2026-07-27 00:52:33 +00:00
263 lines
7.7 KiB
TypeScript
263 lines
7.7 KiB
TypeScript
/**
|
|
* 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 React, { useState, useMemo } from 'react';
|
|
import { Card, Row, Col, Statistic, Table, Tag, Input, Select } from 'antd';
|
|
import {
|
|
AppstoreOutlined,
|
|
ApiOutlined,
|
|
PictureOutlined,
|
|
PlayCircleOutlined,
|
|
SearchOutlined,
|
|
} from '@ant-design/icons';
|
|
import type { ComponentData, ComponentEntry } from './types';
|
|
|
|
interface ComponentIndexProps {
|
|
data: ComponentData;
|
|
}
|
|
|
|
const CATEGORY_COLORS: Record<string, string> = {
|
|
ui: 'blue',
|
|
'design-system': 'green',
|
|
extension: 'purple',
|
|
};
|
|
|
|
const CATEGORY_LABELS: Record<string, string> = {
|
|
ui: 'Core',
|
|
'design-system': 'Layout',
|
|
extension: 'Extension',
|
|
};
|
|
|
|
const ComponentIndex: React.FC<ComponentIndexProps> = ({ data }) => {
|
|
const [searchText, setSearchText] = useState('');
|
|
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
|
|
|
|
const { statistics, components } = data;
|
|
|
|
const filteredComponents = useMemo(() => {
|
|
return components
|
|
.filter((comp) => {
|
|
const matchesSearch =
|
|
!searchText ||
|
|
comp.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
comp.description.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
comp.package.toLowerCase().includes(searchText.toLowerCase());
|
|
const matchesCategory =
|
|
!categoryFilter || comp.category === categoryFilter;
|
|
return matchesSearch && matchesCategory;
|
|
})
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
}, [components, searchText, categoryFilter]);
|
|
|
|
const { categories, categoryCounts } = useMemo(() => {
|
|
const counts: Record<string, number> = {};
|
|
components.forEach((comp) => {
|
|
counts[comp.category] = (counts[comp.category] || 0) + 1;
|
|
});
|
|
return {
|
|
categories: Object.keys(counts).sort(),
|
|
categoryCounts: counts,
|
|
};
|
|
}, [components]);
|
|
|
|
const columns = [
|
|
{
|
|
title: 'Component',
|
|
dataIndex: 'name',
|
|
key: 'name',
|
|
sorter: (a: ComponentEntry, b: ComponentEntry) =>
|
|
a.name.localeCompare(b.name),
|
|
defaultSortOrder: 'ascend' as const,
|
|
render: (name: string, record: ComponentEntry) => (
|
|
<div>
|
|
<a href={`/${record.docPath}`}>
|
|
<strong>{name}</strong>
|
|
</a>
|
|
{record.description && (
|
|
<div style={{ fontSize: '12px', color: '#666' }}>
|
|
{record.description.slice(0, 100)}
|
|
{record.description.length > 100 ? '...' : ''}
|
|
</div>
|
|
)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: 'Category',
|
|
dataIndex: 'category',
|
|
key: 'category',
|
|
width: 120,
|
|
filters: categories.map((cat) => ({
|
|
text: CATEGORY_LABELS[cat] || cat,
|
|
value: cat,
|
|
})),
|
|
onFilter: (value: React.Key | boolean, record: ComponentEntry) =>
|
|
record.category === value,
|
|
render: (cat: string) => (
|
|
<Tag color={CATEGORY_COLORS[cat] || 'default'}>
|
|
{CATEGORY_LABELS[cat] || cat}
|
|
</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: 'Package',
|
|
dataIndex: 'package',
|
|
key: 'package',
|
|
width: 220,
|
|
render: (pkg: string) => (
|
|
<code style={{ fontSize: '12px' }}>{pkg}</code>
|
|
),
|
|
},
|
|
{
|
|
title: 'Tags',
|
|
key: 'tags',
|
|
width: 280,
|
|
filters: [
|
|
{ text: 'Extension Compatible', value: 'extension' },
|
|
{ text: 'Gallery', value: 'gallery' },
|
|
{ text: 'Live Demo', value: 'demo' },
|
|
],
|
|
onFilter: (value: React.Key | boolean, record: ComponentEntry) => {
|
|
switch (value) {
|
|
case 'extension':
|
|
return record.extensionCompatible;
|
|
case 'gallery':
|
|
return record.hasGallery;
|
|
case 'demo':
|
|
return record.hasLiveExample;
|
|
default:
|
|
return true;
|
|
}
|
|
},
|
|
render: (_: unknown, record: ComponentEntry) => (
|
|
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
|
{record.extensionCompatible && (
|
|
<Tag color="purple">Extension Compatible</Tag>
|
|
)}
|
|
{record.hasGallery && <Tag color="cyan">Gallery</Tag>}
|
|
{record.hasLiveExample && <Tag color="green">Demo</Tag>}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: 'Props',
|
|
dataIndex: 'propsCount',
|
|
key: 'propsCount',
|
|
width: 80,
|
|
sorter: (a: ComponentEntry, b: ComponentEntry) =>
|
|
a.propsCount - b.propsCount,
|
|
render: (count: number) => (
|
|
<span style={{ color: count > 0 ? '#1890ff' : '#999' }}>{count}</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="component-index">
|
|
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
|
<Col xs={12} sm={6}>
|
|
<Card>
|
|
<Statistic
|
|
title="Total Components"
|
|
value={statistics.totalComponents}
|
|
prefix={<AppstoreOutlined />}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={12} sm={6}>
|
|
<Card>
|
|
<Statistic
|
|
title="Extension Compatible"
|
|
value={statistics.extensionCompatible}
|
|
prefix={<ApiOutlined />}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={12} sm={6}>
|
|
<Card>
|
|
<Statistic
|
|
title="With Gallery"
|
|
value={statistics.withGallery}
|
|
prefix={<PictureOutlined />}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={12} sm={6}>
|
|
<Card>
|
|
<Statistic
|
|
title="With Live Demo"
|
|
value={statistics.withLiveExample}
|
|
prefix={<PlayCircleOutlined />}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
|
<Col xs={24} sm={12}>
|
|
<Input
|
|
placeholder="Search components..."
|
|
prefix={<SearchOutlined />}
|
|
value={searchText}
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
allowClear
|
|
/>
|
|
</Col>
|
|
<Col xs={24} sm={12}>
|
|
<Select
|
|
placeholder="Filter by category"
|
|
style={{ width: '100%' }}
|
|
value={categoryFilter}
|
|
onChange={setCategoryFilter}
|
|
allowClear
|
|
options={categories.map((cat) => ({
|
|
label: (
|
|
<span>
|
|
<Tag
|
|
color={CATEGORY_COLORS[cat] || 'default'}
|
|
style={{ marginRight: 8 }}
|
|
>
|
|
{categoryCounts[cat] || 0}
|
|
</Tag>
|
|
{CATEGORY_LABELS[cat] || cat}
|
|
</span>
|
|
),
|
|
value: cat,
|
|
}))}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Table
|
|
dataSource={filteredComponents}
|
|
columns={columns}
|
|
rowKey="name"
|
|
pagination={{
|
|
defaultPageSize: 20,
|
|
showSizeChanger: true,
|
|
showTotal: (total) => `${total} components`,
|
|
}}
|
|
size="middle"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ComponentIndex;
|