fix: error after changed table mode in overview (#30)
Co-authored-by: yaojiping <yaojiping@infini.ltd>
This commit is contained in:
parent
cfdc1870a9
commit
a08e33a570
|
@ -301,6 +301,7 @@ export default forwardRef((props: IProps, ref: any) => {
|
|||
setSelectedItem(item);
|
||||
drawRef.current?.open();
|
||||
}}
|
||||
parentLoading={loading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,303 +1,233 @@
|
|||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Table, Tooltip, Progress } from "antd";
|
||||
import { formatter } from "@/utils/format";
|
||||
import { formatUtcTimeToLocal } from "@/utils/utils";
|
||||
import { formatMessage } from "umi/locale";
|
||||
import { SearchEngineIcon } from "@/lib/search_engines";
|
||||
import { HealthStatusView } from "@/components/infini/health_status_view";
|
||||
import { StatusBlockGroup } from "@/components/infini/status_block";
|
||||
import { Providers, ProviderIcon } from "@/lib/providers";
|
||||
import request from "@/utils/request";
|
||||
import styles from "./index.less"
|
||||
import CommonTable from "../../components/CommonTable";
|
||||
|
||||
export default (props) => {
|
||||
const {
|
||||
dataSource,
|
||||
total,
|
||||
from,
|
||||
pageSize,
|
||||
loading,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
onRowClick,
|
||||
infoAction
|
||||
} = props;
|
||||
|
||||
const [infos, setInfos] = useState({});
|
||||
|
||||
const fetchListInfo = async (data) => {
|
||||
const res = await Promise.all(data?.map((item) => request(infoAction, {
|
||||
method: "POST",
|
||||
body: [item.id],
|
||||
}, false, false)));
|
||||
if (res) {
|
||||
let newInfos = {}
|
||||
res.forEach((item) => {
|
||||
if (item && !item.error) {
|
||||
newInfos = {
|
||||
...newInfos,
|
||||
...item
|
||||
}
|
||||
}
|
||||
})
|
||||
setInfos(newInfos);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchListInfo(dataSource);
|
||||
}, [JSON.stringify(dataSource)])
|
||||
|
||||
const [tableData] = useMemo(() => {
|
||||
let tableData = dataSource?.map((item) => {
|
||||
const id = item?._id;
|
||||
const metadata = item._source || {};
|
||||
const info = id && infos[id] ? infos[id] : {};
|
||||
const summary = info.summary || {};
|
||||
const metrics = info.metrics || {};
|
||||
const fs_total_in_bytes = summary?.fs?.total_in_bytes || 0;
|
||||
const fs_available_in_bytes = summary?.fs?.available_in_bytes || 0;
|
||||
const fs_used_in_bytes = fs_total_in_bytes - fs_available_in_bytes;
|
||||
const jvm_mem_total_in_bytes = summary?.jvm?.heap_max_in_bytes || 0;
|
||||
const jvm_mem_used_in_bytes = summary?.jvm?.heap_used_in_bytes || 0;
|
||||
|
||||
const disk_percent =
|
||||
fs_total_in_bytes > 0
|
||||
? Math.round((fs_used_in_bytes / fs_total_in_bytes) * 100)
|
||||
: 0;
|
||||
const jvm_mem_percent =
|
||||
jvm_mem_total_in_bytes > 0
|
||||
? Math.round((jvm_mem_used_in_bytes / jvm_mem_total_in_bytes) * 100)
|
||||
: 0;
|
||||
|
||||
const metrics_status = metrics?.status || {};
|
||||
|
||||
return {
|
||||
id,
|
||||
metadata,
|
||||
summary,
|
||||
metrics_status,
|
||||
fs_total_in_bytes,
|
||||
fs_available_in_bytes,
|
||||
fs_used_in_bytes,
|
||||
jvm_mem_total_in_bytes,
|
||||
jvm_mem_used_in_bytes,
|
||||
disk_percent,
|
||||
jvm_mem_percent,
|
||||
};
|
||||
});
|
||||
return [tableData];
|
||||
}, [JSON.stringify(dataSource), JSON.stringify(infos)]);
|
||||
|
||||
const [columns] = useMemo(() => {
|
||||
let columns = [
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={
|
||||
<span>
|
||||
Host: {record.metadata?.host}
|
||||
<br />
|
||||
Provider:{" "}
|
||||
{formatMessage({
|
||||
id: `cluster.providers.${record.metadata?.location
|
||||
?.provider ?? Providers.OnPremises}`,
|
||||
})}
|
||||
<br />
|
||||
Region: {record.metadata?.location?.region ?? ""}
|
||||
<br />
|
||||
Version: {record.metadata?.version ?? ""}
|
||||
<br />
|
||||
Tags:{" "}
|
||||
{record.metadata?.tags ? record.metadata.tags.toString() : ""}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div style={{ display: "flex", alignContent: "center", gap: 5 }}>
|
||||
<SearchEngineIcon
|
||||
distribution={record.metadata?.distribution}
|
||||
width="20px"
|
||||
height="20px"
|
||||
/>
|
||||
<span>{record.metadata?.name}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Version",
|
||||
dataIndex: "version",
|
||||
render: (text, record) => {
|
||||
return record.metadata?.version ?? "";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Health",
|
||||
dataIndex: "health_status",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
<StatusBlockGroup data={record.metrics_status?.data} />
|
||||
<span>
|
||||
{record.metrics_status?.metric?.label +
|
||||
"(" +
|
||||
(record.metrics_status?.data?.length || 14) +
|
||||
" " +
|
||||
record.metrics_status?.metric?.units +
|
||||
")"}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<HealthStatusView
|
||||
status={record.metadata?.labels?.health_status}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Nodes",
|
||||
dataIndex: "nodes",
|
||||
render: (text, record) => {
|
||||
return record.summary?.number_of_nodes || 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Indices",
|
||||
dataIndex: "Indices",
|
||||
render: (text, record) => {
|
||||
return record.summary?.number_of_indices || 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Shards",
|
||||
dataIndex: "Shards",
|
||||
render: (text, record) => {
|
||||
return record.summary?.number_of_shards || 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Docs",
|
||||
dataIndex: "Docs",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={`Docs:${formatter.number(
|
||||
record.summary?.number_of_documents
|
||||
)}`}
|
||||
>
|
||||
{formatter.numberToHuman(record.summary?.number_of_documents)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Disk Usage",
|
||||
dataIndex: "DiskUsage",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Total:{formatter.bytes(record.fs_total_in_bytes)}
|
||||
<br />
|
||||
Used:{formatter.bytes(record.fs_used_in_bytes)}
|
||||
<br />
|
||||
Free:{formatter.bytes(record.fs_available_in_bytes)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Progress
|
||||
strokeLinecap="square"
|
||||
strokeColor="#558EF0"
|
||||
strokeWidth={12}
|
||||
percent={record.disk_percent}
|
||||
format={(percent) => `${percent}%`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "JVM Heap",
|
||||
dataIndex: "JVMHeap",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Total:{formatter.bytes(record.jvm_mem_total_in_bytes)}
|
||||
<br />
|
||||
Used:{formatter.bytes(record.jvm_mem_used_in_bytes)}
|
||||
<br />
|
||||
Free:
|
||||
{formatter.bytes(
|
||||
record.jvm_mem_total_in_bytes - record.jvm_mem_used_in_bytes
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Progress
|
||||
strokeLinecap="square"
|
||||
strokeColor="#00BFB3"
|
||||
strokeWidth={12}
|
||||
percent={record.jvm_mem_percent}
|
||||
format={(percent) => `${percent}%`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return [columns];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="table-wrap">
|
||||
<Table
|
||||
size={"small"}
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey={"id"}
|
||||
pagination={{
|
||||
size: "small",
|
||||
total,
|
||||
pageSize,
|
||||
onChange: onPageChange,
|
||||
showSizeChanger: true,
|
||||
onShowSizeChange: (_, size) => {
|
||||
onPageSizeChange(size);
|
||||
<CommonTable
|
||||
{...props}
|
||||
columns={[
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={
|
||||
<span>
|
||||
Host: {record.metadata?.host}
|
||||
<br />
|
||||
Provider:{" "}
|
||||
{formatMessage({
|
||||
id: `cluster.providers.${record.metadata?.location
|
||||
?.provider ?? Providers.OnPremises}`,
|
||||
})}
|
||||
<br />
|
||||
Region: {record.metadata?.location?.region ?? ""}
|
||||
<br />
|
||||
Version: {record.metadata?.version ?? ""}
|
||||
<br />
|
||||
Tags:{" "}
|
||||
{record.metadata?.tags ? record.metadata.tags.toString() : ""}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div style={{ display: "flex", alignContent: "center", gap: 5 }}>
|
||||
<SearchEngineIcon
|
||||
distribution={record.metadata?.distribution}
|
||||
width="20px"
|
||||
height="20px"
|
||||
/>
|
||||
<span>{record.metadata?.name}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
showTotal: (total, range) =>
|
||||
`${range[0]}-${range[1]} of ${total} items`,
|
||||
}}
|
||||
onRow={(record, i) => {
|
||||
},
|
||||
{
|
||||
title: "Version",
|
||||
dataIndex: "version",
|
||||
render: (text, record) => {
|
||||
return record.metadata?.version ?? "";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Health",
|
||||
dataIndex: "health_status",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
<StatusBlockGroup data={record.metrics_status?.data} />
|
||||
<span>
|
||||
{record.metrics_status?.metric?.label +
|
||||
"(" +
|
||||
(record.metrics_status?.data?.length || 14) +
|
||||
" " +
|
||||
record.metrics_status?.metric?.units +
|
||||
")"}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<HealthStatusView
|
||||
status={record.metadata?.labels?.health_status}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Nodes",
|
||||
dataIndex: "nodes",
|
||||
render: (text, record) => {
|
||||
return record.summary?.number_of_nodes || 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Indices",
|
||||
dataIndex: "Indices",
|
||||
render: (text, record) => {
|
||||
return record.summary?.number_of_indices || 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Shards",
|
||||
dataIndex: "Shards",
|
||||
render: (text, record) => {
|
||||
return record.summary?.number_of_shards || 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Docs",
|
||||
dataIndex: "Docs",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={`Docs:${formatter.number(
|
||||
record.summary?.number_of_documents
|
||||
)}`}
|
||||
>
|
||||
{formatter.numberToHuman(record.summary?.number_of_documents)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Disk Usage",
|
||||
dataIndex: "DiskUsage",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Total:{formatter.bytes(record.fs_total_in_bytes)}
|
||||
<br />
|
||||
Used:{formatter.bytes(record.fs_used_in_bytes)}
|
||||
<br />
|
||||
Free:{formatter.bytes(record.fs_available_in_bytes)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Progress
|
||||
strokeLinecap="square"
|
||||
strokeColor="#558EF0"
|
||||
strokeWidth={12}
|
||||
percent={record.disk_percent}
|
||||
format={(percent) => `${percent}%`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "JVM Heap",
|
||||
dataIndex: "JVMHeap",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Total:{formatter.bytes(record.jvm_mem_total_in_bytes)}
|
||||
<br />
|
||||
Used:{formatter.bytes(record.jvm_mem_used_in_bytes)}
|
||||
<br />
|
||||
Free:
|
||||
{formatter.bytes(
|
||||
record.jvm_mem_total_in_bytes - record.jvm_mem_used_in_bytes
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Progress
|
||||
strokeLinecap="square"
|
||||
strokeColor="#00BFB3"
|
||||
strokeWidth={12}
|
||||
percent={record.jvm_mem_percent}
|
||||
format={(percent) => `${percent}%`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
formatData={(dataSource, infos) => {
|
||||
const newData = dataSource?.map((item) => {
|
||||
const id = item?._id;
|
||||
const metadata = item._source || {};
|
||||
const info = id && infos[id] ? infos[id] : {};
|
||||
const summary = info.summary || {};
|
||||
const metrics = info.metrics || {};
|
||||
const fs_total_in_bytes = summary?.fs?.total_in_bytes || 0;
|
||||
const fs_available_in_bytes = summary?.fs?.available_in_bytes || 0;
|
||||
const fs_used_in_bytes = fs_total_in_bytes - fs_available_in_bytes;
|
||||
const jvm_mem_total_in_bytes = summary?.jvm?.heap_max_in_bytes || 0;
|
||||
const jvm_mem_used_in_bytes = summary?.jvm?.heap_used_in_bytes || 0;
|
||||
|
||||
const disk_percent =
|
||||
fs_total_in_bytes > 0
|
||||
? Math.round((fs_used_in_bytes / fs_total_in_bytes) * 100)
|
||||
: 0;
|
||||
const jvm_mem_percent =
|
||||
jvm_mem_total_in_bytes > 0
|
||||
? Math.round((jvm_mem_used_in_bytes / jvm_mem_total_in_bytes) * 100)
|
||||
: 0;
|
||||
|
||||
const metrics_status = metrics?.status || {};
|
||||
|
||||
return {
|
||||
onClick: (event) => {
|
||||
onRowClick(dataSource[i]);
|
||||
},
|
||||
id,
|
||||
metadata,
|
||||
summary,
|
||||
metrics_status,
|
||||
fs_total_in_bytes,
|
||||
fs_available_in_bytes,
|
||||
fs_used_in_bytes,
|
||||
jvm_mem_total_in_bytes,
|
||||
jvm_mem_used_in_bytes,
|
||||
disk_percent,
|
||||
jvm_mem_percent,
|
||||
};
|
||||
}}
|
||||
rowClassName={() => styles.rowPointer}
|
||||
/>
|
||||
</div>
|
||||
});
|
||||
return newData
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,208 +1,167 @@
|
|||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Table, Tooltip, Progress, Icon } from "antd";
|
||||
import { Tooltip, Progress, Icon } from "antd";
|
||||
import { formatter } from "@/utils/format";
|
||||
import { formatUtcTimeToLocal } from "@/utils/utils";
|
||||
import { formatMessage } from "umi/locale";
|
||||
import { SearchEngineIcon } from "@/lib/search_engines";
|
||||
import { HealthStatusView } from "@/components/infini/health_status_view";
|
||||
import { StatusBlockGroup } from "@/components/infini/status_block";
|
||||
import CommonTable from "../../components/CommonTable";
|
||||
|
||||
export default (props) => {
|
||||
const {
|
||||
infos,
|
||||
dataSource,
|
||||
total,
|
||||
from,
|
||||
pageSize,
|
||||
loading,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
onRowClick,
|
||||
} = props;
|
||||
|
||||
const [tableData] = useMemo(() => {
|
||||
let tableData = dataSource?.map((item) => {
|
||||
const id = item?._source?.metadata?.index_id;
|
||||
const metadata = item?._source?.metadata || {};
|
||||
const info = id && infos[id] ? infos[id] : {};
|
||||
const summary = info.summary || {};
|
||||
const metrics = info.metrics || {};
|
||||
|
||||
const timestamp = item?._source?.timestamp
|
||||
? formatUtcTimeToLocal(item?._source?.timestamp)
|
||||
: "N/A";
|
||||
const metrics_status = metrics?.status || {};
|
||||
|
||||
return {
|
||||
id,
|
||||
metadata,
|
||||
summary,
|
||||
metrics_status,
|
||||
timestamp,
|
||||
};
|
||||
});
|
||||
return [tableData];
|
||||
}, [JSON.stringify(dataSource), JSON.stringify(infos)]);
|
||||
|
||||
const [columns] = useMemo(() => {
|
||||
let columns = [
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={
|
||||
<span>
|
||||
Cluster: {record.metadata?.cluster_name}
|
||||
<br />
|
||||
Aliases: {record.metadata?.aliases?.join(",")}
|
||||
<br />
|
||||
Timestamp: {record.timestamp}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div style={{ display: "flex", alignContent: "center", gap: 5 }}>
|
||||
<span>
|
||||
<Icon type="table" />
|
||||
</span>
|
||||
<span>{record.metadata?.index_name}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Health",
|
||||
dataIndex: "health_status",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
<StatusBlockGroup data={record.metrics_status?.data} />
|
||||
<span>
|
||||
{record.metrics_status?.metric?.label +
|
||||
"(" +
|
||||
(record.metrics_status?.data?.length || 14) +
|
||||
" " +
|
||||
record.metrics_status?.metric?.units +
|
||||
")"}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<HealthStatusView
|
||||
status={record.metadata?.labels?.health_status}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
dataIndex: "index_status",
|
||||
render: (text, record) => {
|
||||
return record.metadata?.labels?.state ?? "N/A";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Store Size",
|
||||
dataIndex: "store_size",
|
||||
render: (text, record) => {
|
||||
return record.summary?.index_info?.store_size?.toUpperCase() || "N/A";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Shards",
|
||||
dataIndex: "Shards",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Unassigned Shards:
|
||||
{record.summary?.unassigned_shards || 0}
|
||||
<br />
|
||||
Shards:
|
||||
{record.summary?.index_info?.shards || 0}
|
||||
<br />
|
||||
Replicas:
|
||||
{record.summary?.index_info?.replicas || 0}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{record.summary?.unassigned_shards || 0} /{" "}
|
||||
{record.summary?.index_info?.shards ||
|
||||
0 + (record.summary?.index_info?.replicas || 0)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Docs",
|
||||
dataIndex: "Docs",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Deleted:
|
||||
{formatter.number(record.summary?.docs?.deleted || 0)}
|
||||
<br />
|
||||
Total:
|
||||
{formatter.number(record.summary?.docs?.count || 0)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{formatter.numberToHuman(record.summary?.docs?.deleted)} /{" "}
|
||||
{formatter.numberToHuman(record.summary?.docs?.count)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return [columns];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="table-wrap">
|
||||
<Table
|
||||
size={"small"}
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey={"id"}
|
||||
pagination={{
|
||||
size: "small",
|
||||
total,
|
||||
pageSize,
|
||||
onChange: onPageChange,
|
||||
showSizeChanger: true,
|
||||
onShowSizeChange: (_, size) => {
|
||||
onPageSizeChange(size);
|
||||
<CommonTable
|
||||
{...props}
|
||||
columns={[
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={
|
||||
<span>
|
||||
Cluster: {record.metadata?.cluster_name}
|
||||
<br />
|
||||
Aliases: {record.metadata?.aliases?.join(",")}
|
||||
<br />
|
||||
Timestamp: {record.timestamp}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div style={{ display: "flex", alignContent: "center", gap: 5 }}>
|
||||
<span>
|
||||
<Icon type="table" />
|
||||
</span>
|
||||
<span>{record.metadata?.index_name}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
showTotal: (total, range) =>
|
||||
`${range[0]}-${range[1]} of ${total} items`,
|
||||
}}
|
||||
onRow={(record, i) => {
|
||||
},
|
||||
{
|
||||
title: "Health",
|
||||
dataIndex: "health_status",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
<StatusBlockGroup data={record.metrics_status?.data} />
|
||||
<span>
|
||||
{record.metrics_status?.metric?.label +
|
||||
"(" +
|
||||
(record.metrics_status?.data?.length || 14) +
|
||||
" " +
|
||||
record.metrics_status?.metric?.units +
|
||||
")"}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<HealthStatusView
|
||||
status={record.metadata?.labels?.health_status}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
dataIndex: "index_status",
|
||||
render: (text, record) => {
|
||||
return record.metadata?.labels?.state ?? "N/A";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Store Size",
|
||||
dataIndex: "store_size",
|
||||
render: (text, record) => {
|
||||
return record.summary?.index_info?.store_size?.toUpperCase() || "N/A";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Shards",
|
||||
dataIndex: "Shards",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Unassigned Shards:
|
||||
{record.summary?.unassigned_shards || 0}
|
||||
<br />
|
||||
Shards:
|
||||
{record.summary?.index_info?.shards || 0}
|
||||
<br />
|
||||
Replicas:
|
||||
{record.summary?.index_info?.replicas || 0}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{record.summary?.unassigned_shards || 0} /{" "}
|
||||
{record.summary?.index_info?.shards ||
|
||||
0 + (record.summary?.index_info?.replicas || 0)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Docs",
|
||||
dataIndex: "Docs",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Deleted:
|
||||
{formatter.number(record.summary?.docs?.deleted || 0)}
|
||||
<br />
|
||||
Total:
|
||||
{formatter.number(record.summary?.docs?.count || 0)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{formatter.numberToHuman(record.summary?.docs?.deleted)} /{" "}
|
||||
{formatter.numberToHuman(record.summary?.docs?.count)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
formatData={(dataSource, infos) => {
|
||||
const newData = dataSource?.map((item) => {
|
||||
const id = item?._source?.metadata?.index_id;
|
||||
const metadata = item?._source?.metadata || {};
|
||||
const info = id && infos[id] ? infos[id] : {};
|
||||
const summary = info.summary || {};
|
||||
const metrics = info.metrics || {};
|
||||
|
||||
const timestamp = item?._source?.timestamp
|
||||
? formatUtcTimeToLocal(item?._source?.timestamp)
|
||||
: "N/A";
|
||||
const metrics_status = metrics?.status || {};
|
||||
|
||||
return {
|
||||
onClick: (event) => {
|
||||
onRowClick(dataSource[i]);
|
||||
},
|
||||
id,
|
||||
metadata,
|
||||
summary,
|
||||
metrics_status,
|
||||
timestamp,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
});
|
||||
return newData
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,257 +1,213 @@
|
|||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Table, Tooltip, Progress, Icon } from "antd";
|
||||
import { Tooltip, Progress, Icon } from "antd";
|
||||
import { formatter } from "@/utils/format";
|
||||
import { formatUtcTimeToLocal } from "@/utils/utils";
|
||||
import { formatMessage } from "umi/locale";
|
||||
import { SearchEngineIcon } from "@/lib/search_engines";
|
||||
import { HealthStatusView } from "@/components/infini/health_status_view";
|
||||
import { StatusBlockGroup } from "@/components/infini/status_block";
|
||||
import CommonTable from "../../components/CommonTable";
|
||||
|
||||
export default (props) => {
|
||||
const {
|
||||
infos,
|
||||
dataSource,
|
||||
total,
|
||||
from,
|
||||
pageSize,
|
||||
loading,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
onRowClick,
|
||||
} = props;
|
||||
|
||||
const [tableData] = useMemo(() => {
|
||||
let tableData = dataSource?.map((item) => {
|
||||
const id = item?._source?.metadata?.node_id;
|
||||
const metadata = item._source?.metadata || {};
|
||||
const info = id && infos[id] ? infos[id] : {};
|
||||
const summary = info.summary || {};
|
||||
const metrics = info.metrics || {};
|
||||
const fs_total_in_bytes = summary?.fs?.total?.total_in_bytes || 0;
|
||||
const fs_available_in_bytes = summary?.fs?.total?.available_in_bytes || 0;
|
||||
const fs_used_in_bytes = fs_total_in_bytes - fs_available_in_bytes;
|
||||
const jvm_mem_total_in_bytes = summary?.jvm?.mem?.heap_max_in_bytes || 0;
|
||||
const jvm_mem_used_in_bytes = summary?.jvm?.mem?.heap_used_in_bytes || 0;
|
||||
|
||||
const disk_percent =
|
||||
fs_total_in_bytes > 0
|
||||
? Math.round((fs_used_in_bytes / fs_total_in_bytes) * 100)
|
||||
: 0;
|
||||
const jvm_mem_percent =
|
||||
jvm_mem_total_in_bytes > 0
|
||||
? Math.round((jvm_mem_used_in_bytes / jvm_mem_total_in_bytes) * 100)
|
||||
: 0;
|
||||
|
||||
const metrics_status = metrics?.status || {};
|
||||
|
||||
return {
|
||||
id,
|
||||
metadata,
|
||||
summary,
|
||||
metrics_status,
|
||||
fs_total_in_bytes,
|
||||
fs_available_in_bytes,
|
||||
fs_used_in_bytes,
|
||||
jvm_mem_total_in_bytes,
|
||||
jvm_mem_used_in_bytes,
|
||||
disk_percent,
|
||||
jvm_mem_percent,
|
||||
};
|
||||
});
|
||||
return [tableData];
|
||||
}, [JSON.stringify(dataSource), JSON.stringify(infos)]);
|
||||
|
||||
const [columns] = useMemo(() => {
|
||||
let columns = [
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={
|
||||
<span>
|
||||
Transport Address:{" "}
|
||||
{record.metadata?.labels?.transport_address}
|
||||
<br />
|
||||
Cluster: {record.metadata?.cluster_name ?? ""}
|
||||
<br />
|
||||
Version: {record.metadata?.labels?.version ?? ""}
|
||||
<br />
|
||||
Roles:{" "}
|
||||
{record.metadata?.labels?.roles
|
||||
? record.metadata?.labels?.roles?.toString()
|
||||
: ""}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div style={{ display: "flex", alignContent: "center", gap: 5 }}>
|
||||
<span>
|
||||
{record.summary?.is_master_node ? (
|
||||
<Icon type="star" theme="filled" />
|
||||
) : (
|
||||
<Icon type="database" />
|
||||
)}
|
||||
</span>
|
||||
<span>{record.metadata?.node_name}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
dataIndex: "status",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
<StatusBlockGroup data={record.metrics_status?.data} />
|
||||
<span>
|
||||
{record.metrics_status?.metric?.label +
|
||||
"(" +
|
||||
(record.metrics_status?.data?.length || 14) +
|
||||
" " +
|
||||
record.metrics_status?.metric?.units +
|
||||
")"}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<HealthStatusView status={record.metadata?.labels?.status} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Indices",
|
||||
dataIndex: "Indices",
|
||||
render: (text, record) => {
|
||||
return record.summary?.shard_info?.indices_count || 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Shards",
|
||||
dataIndex: "Shards",
|
||||
render: (text, record) => {
|
||||
return record.summary?.shard_info?.shard_count || 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Docs",
|
||||
dataIndex: "Docs",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={`Docs:${formatter.number(
|
||||
record.summary?.indices?.docs?.count
|
||||
)}`}
|
||||
>
|
||||
{formatter.numberToHuman(record.summary?.indices?.docs?.count)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Disk Usage",
|
||||
dataIndex: "DiskUsage",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Total:{formatter.bytes(record.fs_total_in_bytes)}
|
||||
<br />
|
||||
Used:{formatter.bytes(record.fs_used_in_bytes)}
|
||||
<br />
|
||||
Free:{formatter.bytes(record.fs_available_in_bytes)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Progress
|
||||
strokeLinecap="square"
|
||||
strokeColor="#558EF0"
|
||||
strokeWidth={12}
|
||||
percent={record.disk_percent}
|
||||
format={(percent) => `${percent}%`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "JVM Heap",
|
||||
dataIndex: "JVMHeap",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Total:{formatter.bytes(record.jvm_mem_total_in_bytes)}
|
||||
<br />
|
||||
Used:{formatter.bytes(record.jvm_mem_used_in_bytes)}
|
||||
<br />
|
||||
Free:
|
||||
{formatter.bytes(
|
||||
record.jvm_mem_total_in_bytes - record.jvm_mem_used_in_bytes
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Progress
|
||||
strokeLinecap="square"
|
||||
strokeColor="#00BFB3"
|
||||
strokeWidth={12}
|
||||
percent={record.jvm_mem_percent}
|
||||
format={(percent) => `${percent}%`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return [columns];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="table-wrap">
|
||||
<Table
|
||||
size={"small"}
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey={"id"}
|
||||
pagination={{
|
||||
size: "small",
|
||||
total,
|
||||
pageSize,
|
||||
onChange: onPageChange,
|
||||
showSizeChanger: true,
|
||||
onShowSizeChange: (_, size) => {
|
||||
onPageSizeChange(size);
|
||||
<CommonTable
|
||||
{...props}
|
||||
columns={[
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
title={
|
||||
<span>
|
||||
Transport Address:{" "}
|
||||
{record.metadata?.labels?.transport_address}
|
||||
<br />
|
||||
Cluster: {record.metadata?.cluster_name ?? ""}
|
||||
<br />
|
||||
Version: {record.metadata?.labels?.version ?? ""}
|
||||
<br />
|
||||
Roles:{" "}
|
||||
{record.metadata?.labels?.roles
|
||||
? record.metadata?.labels?.roles?.toString()
|
||||
: ""}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div style={{ display: "flex", alignContent: "center", gap: 5 }}>
|
||||
<span>
|
||||
{record.summary?.is_master_node ? (
|
||||
<Icon type="star" theme="filled" />
|
||||
) : (
|
||||
<Icon type="database" />
|
||||
)}
|
||||
</span>
|
||||
<span>{record.metadata?.node_name}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
showTotal: (total, range) =>
|
||||
`${range[0]}-${range[1]} of ${total} items`,
|
||||
}}
|
||||
onRow={(record, i) => {
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
dataIndex: "status",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
<StatusBlockGroup data={record.metrics_status?.data} />
|
||||
<span>
|
||||
{record.metrics_status?.metric?.label +
|
||||
"(" +
|
||||
(record.metrics_status?.data?.length || 14) +
|
||||
" " +
|
||||
record.metrics_status?.metric?.units +
|
||||
")"}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<HealthStatusView status={record.metadata?.labels?.status} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Indices",
|
||||
dataIndex: "Indices",
|
||||
render: (text, record) => {
|
||||
return record.summary?.shard_info?.indices_count || 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Shards",
|
||||
dataIndex: "Shards",
|
||||
render: (text, record) => {
|
||||
return record.summary?.shard_info?.shard_count || 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Docs",
|
||||
dataIndex: "Docs",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={`Docs:${formatter.number(
|
||||
record.summary?.indices?.docs?.count
|
||||
)}`}
|
||||
>
|
||||
{formatter.numberToHuman(record.summary?.indices?.docs?.count)}
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Disk Usage",
|
||||
dataIndex: "DiskUsage",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Total:{formatter.bytes(record.fs_total_in_bytes)}
|
||||
<br />
|
||||
Used:{formatter.bytes(record.fs_used_in_bytes)}
|
||||
<br />
|
||||
Free:{formatter.bytes(record.fs_available_in_bytes)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Progress
|
||||
strokeLinecap="square"
|
||||
strokeColor="#558EF0"
|
||||
strokeWidth={12}
|
||||
percent={record.disk_percent}
|
||||
format={(percent) => `${percent}%`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "JVM Heap",
|
||||
dataIndex: "JVMHeap",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Total:{formatter.bytes(record.jvm_mem_total_in_bytes)}
|
||||
<br />
|
||||
Used:{formatter.bytes(record.jvm_mem_used_in_bytes)}
|
||||
<br />
|
||||
Free:
|
||||
{formatter.bytes(
|
||||
record.jvm_mem_total_in_bytes - record.jvm_mem_used_in_bytes
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Progress
|
||||
strokeLinecap="square"
|
||||
strokeColor="#00BFB3"
|
||||
strokeWidth={12}
|
||||
percent={record.jvm_mem_percent}
|
||||
format={(percent) => `${percent}%`}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
formatData={(dataSource, infos) => {
|
||||
const newData = dataSource?.map((item) => {
|
||||
const id = item?._source?.metadata?.node_id;
|
||||
const metadata = item._source?.metadata || {};
|
||||
const info = id && infos[id] ? infos[id] : {};
|
||||
const summary = info.summary || {};
|
||||
const metrics = info.metrics || {};
|
||||
const fs_total_in_bytes = summary?.fs?.total?.total_in_bytes || 0;
|
||||
const fs_available_in_bytes = summary?.fs?.total?.available_in_bytes || 0;
|
||||
const fs_used_in_bytes = fs_total_in_bytes - fs_available_in_bytes;
|
||||
const jvm_mem_total_in_bytes = summary?.jvm?.mem?.heap_max_in_bytes || 0;
|
||||
const jvm_mem_used_in_bytes = summary?.jvm?.mem?.heap_used_in_bytes || 0;
|
||||
|
||||
const disk_percent =
|
||||
fs_total_in_bytes > 0
|
||||
? Math.round((fs_used_in_bytes / fs_total_in_bytes) * 100)
|
||||
: 0;
|
||||
const jvm_mem_percent =
|
||||
jvm_mem_total_in_bytes > 0
|
||||
? Math.round((jvm_mem_used_in_bytes / jvm_mem_total_in_bytes) * 100)
|
||||
: 0;
|
||||
|
||||
const metrics_status = metrics?.status || {};
|
||||
|
||||
return {
|
||||
onClick: (event) => {
|
||||
onRowClick(dataSource[i]);
|
||||
},
|
||||
id,
|
||||
metadata,
|
||||
summary,
|
||||
metrics_status,
|
||||
fs_total_in_bytes,
|
||||
fs_available_in_bytes,
|
||||
fs_used_in_bytes,
|
||||
jvm_mem_total_in_bytes,
|
||||
jvm_mem_used_in_bytes,
|
||||
disk_percent,
|
||||
jvm_mem_percent,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
});
|
||||
return newData
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Table, Tooltip, Progress } from "antd";
|
||||
import { formatter } from "@/utils/format";
|
||||
import { formatUtcTimeToLocal } from "@/utils/utils";
|
||||
import { formatMessage } from "umi/locale";
|
||||
import { SearchEngineIcon } from "@/lib/search_engines";
|
||||
import { HealthStatusView } from "@/components/infini/health_status_view";
|
||||
import { StatusBlockGroup } from "@/components/infini/status_block";
|
||||
import { Providers, ProviderIcon } from "@/lib/providers";
|
||||
import request from "@/utils/request";
|
||||
import styles from "./index.less"
|
||||
|
||||
export default (props) => {
|
||||
const {
|
||||
dataSource,
|
||||
total,
|
||||
from,
|
||||
pageSize,
|
||||
loading,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
onRowClick,
|
||||
infoAction,
|
||||
formatData,
|
||||
columns = [],
|
||||
parentLoading
|
||||
} = props;
|
||||
|
||||
const [infos, setInfos] = useState({});
|
||||
|
||||
const fetchListInfo = async (data) => {
|
||||
const res = await Promise.all(data?.map((item) => request(infoAction, {
|
||||
method: "POST",
|
||||
body: [item.id],
|
||||
}, false, false)));
|
||||
if (res) {
|
||||
let newInfos = {}
|
||||
res.forEach((item) => {
|
||||
if (item && !item.error) {
|
||||
newInfos = {
|
||||
...newInfos,
|
||||
...item
|
||||
}
|
||||
}
|
||||
})
|
||||
setInfos(newInfos);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!parentLoading) {
|
||||
fetchListInfo(dataSource);
|
||||
}
|
||||
}, [JSON.stringify(dataSource), parentLoading])
|
||||
|
||||
const tableData = useMemo(() => {
|
||||
return formatData(dataSource, infos);
|
||||
}, [JSON.stringify(dataSource), JSON.stringify(infos)]);
|
||||
|
||||
return (
|
||||
<div className="table-wrap">
|
||||
<Table
|
||||
size={"small"}
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
rowKey={"id"}
|
||||
pagination={{
|
||||
size: "small",
|
||||
total,
|
||||
pageSize,
|
||||
onChange: onPageChange,
|
||||
showSizeChanger: true,
|
||||
onShowSizeChange: (_, size) => {
|
||||
onPageSizeChange(size);
|
||||
},
|
||||
showTotal: (total, range) =>
|
||||
`${range[0]}-${range[1]} of ${total} items`,
|
||||
}}
|
||||
onRow={(record, i) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
onRowClick(dataSource[i]);
|
||||
},
|
||||
};
|
||||
}}
|
||||
rowClassName={() => styles.rowPointer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue