This is an automated email from the ASF dual-hosted git repository.
arafat2198 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 7a26afff5a HDDS-11158. Improve Pipelines page UI (#7171)
7a26afff5a is described below
commit 7a26afff5a06da2876d53cce69bc79efa1953251
Author: Abhishek Pal <[email protected]>
AuthorDate: Wed Sep 18 13:50:00 2024 +0530
HDDS-11158. Improve Pipelines page UI (#7171)
---
.../src/v2/components/search/search.tsx | 2 +
.../src/v2/components/tables/bucketsTable.tsx | 267 +++++++++++++++++++++
.../src/v2/components/tables/pipelinesTable.tsx | 211 ++++++++++++++++
.../src/v2/components/tables/volumesTable.tsx | 179 ++++++++++++++
.../src/v2/pages/buckets/buckets.tsx | 236 +-----------------
.../pipelines/pipelines.less} | 48 ++--
.../src/v2/pages/pipelines/pipelines.tsx | 160 ++++++++++++
.../src/v2/pages/volumes/volumes.tsx | 147 ++----------
.../recon/ozone-recon-web/src/v2/routes-v2.tsx | 5 +
.../ozone-recon-web/src/v2/types/bucket.types.ts | 12 +-
.../src/v2/types/pipelines.types.ts | 62 +++++
.../ozone-recon-web/src/v2/types/volume.types.ts | 10 +-
.../ozone-recon-web/src/v2/utils/momentUtils.ts | 63 +++++
13 files changed, 1017 insertions(+), 385 deletions(-)
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
index 8cac2a9c04..d320fd659a 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
@@ -20,6 +20,7 @@ import React from 'react';
import { Input, Select } from 'antd';
import { Option } from '@/v2/components/select/singleSelect';
+import { DownOutlined } from '@ant-design/icons';
// ------------- Types -------------- //
type SearchProps = {
@@ -51,6 +52,7 @@ const Search: React.FC<SearchProps> = ({
const selectFilter = searchColumn
? (<Select
disabled={disabled}
+ suffixIcon={(searchOptions.length > 1) ? <DownOutlined/> : null}
defaultValue={searchColumn}
options={searchOptions}
onChange={onChange} />)
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/bucketsTable.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/bucketsTable.tsx
new file mode 100644
index 0000000000..b26ae251f9
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/bucketsTable.tsx
@@ -0,0 +1,267 @@
+/*
+ * 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 from 'react';
+
+import moment from 'moment';
+import Table, {
+ ColumnProps,
+ ColumnsType,
+ TablePaginationConfig
+} from 'antd/es/table';
+import Tag from 'antd/es/tag';
+import {
+ CheckCircleOutlined,
+ CloseCircleOutlined,
+ CloudServerOutlined,
+ FileUnknownOutlined,
+ HddOutlined,
+ LaptopOutlined,
+ SaveOutlined
+} from '@ant-design/icons';
+
+import QuotaBar from '@/components/quotaBar/quotaBar';
+import { nullAwareLocaleCompare } from '@/utils/common';
+import {
+ Bucket,
+ BucketLayout,
+ BucketLayoutTypeList,
+ BucketsTableProps,
+ BucketStorage,
+ BucketStorageTypeList
+} from '@/v2/types/bucket.types';
+
+function renderIsVersionEnabled(isVersionEnabled: boolean) {
+ return isVersionEnabled
+ ? <CheckCircleOutlined
+ style={{ color: '#1da57a' }}
+ className='icon-success' />
+ : <CloseCircleOutlined className='icon-neutral' />
+};
+
+function renderStorageType(bucketStorage: BucketStorage) {
+ const bucketStorageIconMap: Record<BucketStorage, React.ReactElement> = {
+ RAM_DISK: <LaptopOutlined />,
+ SSD: <SaveOutlined />,
+ DISK: <HddOutlined />,
+ ARCHIVE: <CloudServerOutlined />
+ };
+ const icon = bucketStorage in bucketStorageIconMap
+ ? bucketStorageIconMap[bucketStorage]
+ : <FileUnknownOutlined />;
+ return <span>{icon} {bucketStorage}</span>;
+};
+
+function renderBucketLayout(bucketLayout: BucketLayout) {
+ const bucketLayoutColorMap = {
+ FILE_SYSTEM_OPTIMIZED: 'green',
+ OBJECT_STORE: 'orange',
+ LEGACY: 'blue'
+ };
+ const color = bucketLayout in bucketLayoutColorMap ?
+ bucketLayoutColorMap[bucketLayout] : '';
+ return <Tag color={color}>{bucketLayout}</Tag>;
+};
+
+export const COLUMNS: ColumnsType<Bucket> = [
+ {
+ title: 'Bucket',
+ dataIndex: 'name',
+ key: 'name',
+ sorter: (a: Bucket, b: Bucket) => a.name.localeCompare(b.name),
+ defaultSortOrder: 'ascend' as const
+ },
+ {
+ title: 'Volume',
+ dataIndex: 'volumeName',
+ key: 'volumeName',
+ sorter: (a: Bucket, b: Bucket) => a.volumeName.localeCompare(b.volumeName),
+ defaultSortOrder: 'ascend' as const
+ },
+ {
+ title: 'Owner',
+ dataIndex: 'owner',
+ key: 'owner',
+ sorter: (a: Bucket, b: Bucket) => nullAwareLocaleCompare(a.owner, b.owner)
+ },
+ {
+ title: 'Versioning',
+ dataIndex: 'versioning',
+ key: 'isVersionEnabled',
+ render: (isVersionEnabled: boolean) =>
renderIsVersionEnabled(isVersionEnabled)
+ },
+ {
+ title: 'Storage Type',
+ dataIndex: 'storageType',
+ key: 'storageType',
+ filterMultiple: true,
+ filters: BucketStorageTypeList.map(state => ({ text: state, value: state
})),
+ onFilter: (value, record: Bucket) => record.storageType === value,
+ sorter: (a: Bucket, b: Bucket) =>
a.storageType.localeCompare(b.storageType),
+ render: (storageType: BucketStorage) => renderStorageType(storageType)
+ },
+ {
+ title: 'Bucket Layout',
+ dataIndex: 'bucketLayout',
+ key: 'bucketLayout',
+ filterMultiple: true,
+ filters: BucketLayoutTypeList.map(state => ({ text: state, value: state
})),
+ onFilter: (value, record: Bucket) => record.bucketLayout === value,
+ sorter: (a: Bucket, b: Bucket) =>
a.bucketLayout.localeCompare(b.bucketLayout),
+ render: (bucketLayout: BucketLayout) => renderBucketLayout(bucketLayout)
+ },
+ {
+ title: 'Creation Time',
+ dataIndex: 'creationTime',
+ key: 'creationTime',
+ sorter: (a: Bucket, b: Bucket) => a.creationTime - b.creationTime,
+ render: (creationTime: number) => {
+ return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
+ }
+ },
+ {
+ title: 'Modification Time',
+ dataIndex: 'modificationTime',
+ key: 'modificationTime',
+ sorter: (a: Bucket, b: Bucket) => a.modificationTime - b.modificationTime,
+ render: (modificationTime: number) => {
+ return modificationTime > 0 ? moment(modificationTime).format('ll LTS')
: 'NA';
+ }
+ },
+ {
+ title: 'Storage Capacity',
+ key: 'quotaCapacityBytes',
+ sorter: (a: Bucket, b: Bucket) => a.usedBytes - b.usedBytes,
+ render: (text: string, record: Bucket) => (
+ <QuotaBar
+ quota={record.quotaInBytes}
+ used={record.usedBytes}
+ quotaType='size'
+ />
+ )
+ },
+ {
+ title: 'Namespace Capacity',
+ key: 'namespaceCapacity',
+ sorter: (a: Bucket, b: Bucket) => a.usedNamespace - b.usedNamespace,
+ render: (text: string, record: Bucket) => (
+ <QuotaBar
+ quota={record.quotaInNamespace}
+ used={record.usedNamespace}
+ quotaType='namespace'
+ />
+ )
+ },
+ {
+ title: 'Source Volume',
+ dataIndex: 'sourceVolume',
+ key: 'sourceVolume',
+ render: (sourceVolume: string) => {
+ return sourceVolume ? sourceVolume : 'NA';
+ }
+ },
+ {
+ title: 'Source Bucket',
+ dataIndex: 'sourceBucket',
+ key: 'sourceBucket',
+ render: (sourceBucket: string) => {
+ return sourceBucket ? sourceBucket : 'NA';
+ }
+ }
+];
+
+const BucketsTable: React.FC<BucketsTableProps> = ({
+ loading = false,
+ data,
+ handleAclClick,
+ selectedColumns,
+ searchColumn = 'name',
+ searchTerm = ''
+}) => {
+
+ React.useEffect(() => {
+ const aclColumn: ColumnProps<Bucket> = {
+ title: 'ACLs',
+ dataIndex: 'acls',
+ key: 'acls',
+ render: (_: any, record: Bucket) => {
+ return (
+ <a
+ key='acl'
+ onClick={() => {
+ handleAclClick(record);
+ }}
+ >
+ Show ACL
+ </a>
+ );
+ }
+ };
+
+ if (COLUMNS.length > 0 && COLUMNS[COLUMNS.length - 1].key !== 'acls') {
+ // Push the ACL column for initial load
+ COLUMNS.push(aclColumn);
+ selectedColumns.push({
+ label: aclColumn.title as string,
+ value: aclColumn.key as string
+ });
+ } else {
+ // Replace old ACL column with new ACL column with correct reference
+ // e.g. After page is reloaded / redirect from other page
+ COLUMNS[COLUMNS.length - 1] = aclColumn;
+ selectedColumns[selectedColumns.length - 1] = {
+ label: aclColumn.title as string,
+ value: aclColumn.key as string
+ }
+ }
+ }, []);
+
+ function filterSelectedColumns() {
+ const columnKeys = selectedColumns.map((column) => column.value);
+ return COLUMNS.filter(
+ (column) => columnKeys.indexOf(column.key as string) >= 0
+ )
+ }
+
+ function getFilteredData(data: Bucket[]) {
+ return data.filter(
+ (bucket: Bucket) => bucket[searchColumn].includes(searchTerm)
+ );
+ }
+
+ const paginationConfig: TablePaginationConfig = {
+ showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total}
buckets`,
+ showSizeChanger: true
+ };
+
+ return (
+ <div>
+ <Table
+ dataSource={getFilteredData(data)}
+ columns={filterSelectedColumns()}
+ loading={loading}
+ rowKey='volume'
+ pagination={paginationConfig}
+ scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
+ locale={{ filterTitle: '' }}
+ />
+ </div>
+ )
+}
+
+export default BucketsTable;
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/pipelinesTable.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/pipelinesTable.tsx
new file mode 100644
index 0000000000..6c07749436
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/pipelinesTable.tsx
@@ -0,0 +1,211 @@
+/*
+ * 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 from 'react';
+
+import Table, {
+ ColumnsType,
+ TablePaginationConfig
+} from 'antd/es/table';
+import Tooltip from 'antd/es/tooltip';
+import { InfoCircleOutlined } from '@ant-design/icons';
+
+import { ReplicationIcon } from '@/utils/themeIcons';
+import { getDurationFromTimestamp, getTimeDiffFromTimestamp } from
'@/v2/utils/momentUtils';
+import { Pipeline, PipelinesTableProps, PipelineStatusList } from
'@/v2/types/pipelines.types';
+
+
+// TODO: When Datanodes PR gets merged remove these declarations
+// And import from datanodes.types
+
+type SummaryDatanodeDetails = {
+ level: number;
+ parent: unknown | null;
+ cost: number;
+ uuid: string;
+ uuidString: string;
+ ipAddress: string;
+ hostName: string;
+ ports: {
+ name: string;
+ value: number
+ }[];
+ certSerialId: null,
+ version: string | null;
+ setupTime: number;
+ revision: string | null;
+ buildDate: string;
+ persistedOpState: string;
+ persistedOpStateExpiryEpochSec: number;
+ initialVersion: number;
+ currentVersion: number;
+ signature: number;
+ decommissioned: boolean;
+ networkName: string;
+ networkLocation: string;
+ networkFullPath: string;
+ numOfLeaves: number;
+}
+
+export const COLUMNS: ColumnsType<Pipeline> = [
+ {
+ title: 'Pipeline ID',
+ dataIndex: 'pipelineId',
+ key: 'pipelineId',
+ sorter: (a: Pipeline, b: Pipeline) =>
a.pipelineId.localeCompare(b.pipelineId),
+
+ },
+ {
+ title: 'Replication Type & Factor',
+ dataIndex: 'replicationType',
+ key: 'replicationType',
+ render: (replicationType: string, record: Pipeline) => {
+ const replicationFactor = record.replicationFactor;
+ return (
+ <span>
+ <ReplicationIcon
+ replicationFactor={replicationFactor}
+ replicationType={replicationType}
+ leaderNode={record.leaderNode}
+ isLeader={false} />
+ {replicationType} ({replicationFactor})
+ </span>
+ );
+ },
+ sorter: (a: Pipeline, b: Pipeline) =>
+ (a.replicationType +
a.replicationFactor.toString()).localeCompare(b.replicationType +
b.replicationFactor.toString()),
+ defaultSortOrder: 'descend' as const
+ },
+ {
+ title: 'Status',
+ dataIndex: 'status',
+ key: 'status',
+ filterMultiple: true,
+ filters: PipelineStatusList.map(status => ({ text: status, value: status
})),
+ onFilter: (value, record: Pipeline) => record.status === value,
+ sorter: (a: Pipeline, b: Pipeline) => a.status.localeCompare(b.status)
+ },
+ {
+ title: 'Containers',
+ dataIndex: 'containers',
+ key: 'containers',
+ sorter: (a: Pipeline, b: Pipeline) => a.containers - b.containers
+ },
+ {
+ title: 'Datanodes',
+ dataIndex: 'datanodes',
+ key: 'datanodes',
+ render: (datanodes: SummaryDatanodeDetails[]) => (
+ <div>
+ {datanodes.map(datanode => (
+ <div className='uuid-tooltip'>
+ <Tooltip
+ placement='top'
+ title={`UUID: ${datanode?.uuid ?? 'NA'}`}
+ getPopupContainer={(triggerNode) => triggerNode}>
+ {datanode?.hostName ?? 'N/A'}
+ </Tooltip>
+ </div>
+ ))}
+ </div>
+ )
+ },
+ {
+ title: 'Leader',
+ dataIndex: 'leaderNode',
+ key: 'leaderNode',
+ sorter: (a: Pipeline, b: Pipeline) =>
a.leaderNode.localeCompare(b.leaderNode)
+ },
+ {
+ title: () => (
+ <span>
+ Last Leader Election
+ <Tooltip title='Elapsed time since the current leader got elected.
Only available if any metrics service providers like Prometheus is configured.'>
+ <InfoCircleOutlined />
+ </Tooltip>
+ </span>
+ ),
+ dataIndex: 'lastLeaderElection',
+ key: 'lastLeaderElection',
+ render: (lastLeaderElection: number) => lastLeaderElection > 0 ?
+ getTimeDiffFromTimestamp(lastLeaderElection) : 'NA',
+ sorter: (a: Pipeline, b: Pipeline) => a.lastLeaderElection -
b.lastLeaderElection
+ },
+ {
+ title: 'Lifetime',
+ dataIndex: 'duration',
+ key: 'duration',
+ render: (duration: number) => getDurationFromTimestamp(duration),
+ sorter: (a: Pipeline, b: Pipeline) => a.duration - b.duration
+ },
+ {
+ title: () => (
+ <span>
+ No. of Elections
+ <Tooltip title='Number of elections in this pipeline. Only available
if any metrics service providers like Prometheus is configured.'>
+ <InfoCircleOutlined />
+ </Tooltip>
+ </span>
+ ),
+ dataIndex: 'leaderElections',
+ key: 'leaderElections',
+ render: (leaderElections: number) => leaderElections > 0 ?
+ leaderElections : 'NA',
+ sorter: (a: Pipeline, b: Pipeline) => a.leaderElections - b.leaderElections
+ }
+];
+
+const PipelinesTable: React.FC<PipelinesTableProps> = ({
+ loading = false,
+ data,
+ selectedColumns,
+ searchTerm = ''
+}) => {
+ const paginationConfig: TablePaginationConfig = {
+ showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total}
pipelines`,
+ showSizeChanger: true,
+ };
+
+ function filterSelectedColumns() {
+ const columnKeys = selectedColumns.map((column) => column.value);
+ return COLUMNS.filter(
+ (column) => columnKeys.indexOf(column.key as string) >= 0
+ )
+ }
+
+ function getFilteredData(data: Pipeline[]) {
+ return data.filter(
+ (pipeline: Pipeline) => pipeline['pipelineId'].includes(searchTerm)
+ )
+ }
+
+ return (
+ <div>
+ <Table
+ dataSource={getFilteredData(data)}
+ columns={filterSelectedColumns()}
+ loading={loading}
+ rowKey='pipelineId'
+ pagination={paginationConfig}
+ scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
+ locale={{ filterTitle: '' }} />
+ </div>
+ )
+}
+
+export default PipelinesTable;
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/volumesTable.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/volumesTable.tsx
new file mode 100644
index 0000000000..4de0d713fc
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/volumesTable.tsx
@@ -0,0 +1,179 @@
+/*
+ * 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 QuotaBar from '@/components/quotaBar/quotaBar';
+import { byteToSize } from '@/utils/common';
+import { Volume, VolumesTableProps } from '@/v2/types/volume.types';
+import Table, { ColumnsType, ColumnType, TablePaginationConfig } from
'antd/es/table';
+import moment from 'moment';
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+export const COLUMNS: ColumnsType<Volume> = [
+ {
+ title: 'Volume',
+ dataIndex: 'volume',
+ key: 'volume',
+ sorter: (a: Volume, b: Volume) => a.volume.localeCompare(b.volume),
+ defaultSortOrder: 'ascend' as const,
+ width: '15%'
+ },
+ {
+ title: 'Owner',
+ dataIndex: 'owner',
+ key: 'owner',
+ sorter: (a: Volume, b: Volume) => a.owner.localeCompare(b.owner)
+ },
+ {
+ title: 'Admin',
+ dataIndex: 'admin',
+ key: 'admin',
+ sorter: (a: Volume, b: Volume) => a.admin.localeCompare(b.admin)
+ },
+ {
+ title: 'Creation Time',
+ dataIndex: 'creationTime',
+ key: 'creationTime',
+ sorter: (a: Volume, b: Volume) => a.creationTime - b.creationTime,
+ render: (creationTime: number) => {
+ return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
+ }
+ },
+ {
+ title: 'Modification Time',
+ dataIndex: 'modificationTime',
+ key: 'modificationTime',
+ sorter: (a: Volume, b: Volume) => a.modificationTime - b.modificationTime,
+ render: (modificationTime: number) => {
+ return modificationTime > 0 ? moment(modificationTime).format('ll LTS')
: 'NA';
+ }
+ },
+ {
+ title: 'Quota (Size)',
+ dataIndex: 'quotaInBytes',
+ key: 'quotaInBytes',
+ render: (quotaInBytes: number) => {
+ return quotaInBytes && quotaInBytes !== -1 ? byteToSize(quotaInBytes, 3)
: 'NA';
+ }
+ },
+ {
+ title: 'Namespace Capacity',
+ key: 'namespaceCapacity',
+ sorter: (a: Volume, b: Volume) => a.usedNamespace - b.usedNamespace,
+ render: (text: string, record: Volume) => (
+ <QuotaBar
+ quota={record.quotaInNamespace}
+ used={record.usedNamespace}
+ quotaType='namespace'
+ />
+ )
+ },
+];
+
+const VolumesTable: React.FC<VolumesTableProps> = ({
+ loading = false,
+ data,
+ handleAclClick,
+ selectedColumns,
+ searchColumn = 'volume',
+ searchTerm = ''
+}) => {
+
+ React.useEffect(() => {
+ // On table mount add the actions column
+ console.log("Adding new column");
+ const actionsColumn: ColumnType<Volume> = {
+ title: 'Actions',
+ key: 'actions',
+ render: (_: any, record: Volume) => {
+ const searchParams = new URLSearchParams();
+ searchParams.append('volume', record.volume);
+
+ return (
+ <>
+ <Link
+ key="listBuckets"
+ to={`/Buckets?${searchParams.toString()}`}
+ style={{
+ marginRight: '16px'
+ }}>
+ Show buckets
+ </Link>
+ <a
+ key='acl'
+ onClick={() => handleAclClick(record)}>
+ Show ACL
+ </a>
+ </>
+ );
+ }
+ }
+
+ if (COLUMNS.length > 0 && COLUMNS[COLUMNS.length - 1].key !== 'actions') {
+ // Push the ACL column for initial
+ COLUMNS.push(actionsColumn);
+ selectedColumns.push({
+ label: actionsColumn.title as string,
+ value: actionsColumn.key as string
+ });
+ } else {
+ // Replace old ACL column with new ACL column with correct reference
+ // e.g. After page is reloaded / redirect from other page
+ COLUMNS[COLUMNS.length - 1] = actionsColumn;
+ selectedColumns[selectedColumns.length - 1] = {
+ label: actionsColumn.title as string,
+ value: actionsColumn.key as string
+ }
+ }
+
+ }, []);
+
+ function filterSelectedColumns() {
+ const columnKeys = selectedColumns.map((column) => column.value);
+ return COLUMNS.filter(
+ (column) => columnKeys.indexOf(column.key as string) >= 0
+ )
+ }
+
+ function getFilteredData(data: Volume[]) {
+ return data.filter(
+ (volume: Volume) => volume[searchColumn].includes(searchTerm)
+ );
+ }
+
+ const paginationConfig: TablePaginationConfig = {
+ showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total}
volumes`,
+ showSizeChanger: true
+ };
+
+ return (
+ <div>
+ <Table
+ dataSource={getFilteredData(data)}
+ columns={filterSelectedColumns()}
+ loading={loading}
+ rowKey='volume'
+ pagination={paginationConfig}
+ scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
+ locale={{ filterTitle: '' }}
+ />
+ </div>
+ )
+}
+
+export default VolumesTable;
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx
index bd8950e54c..12af3bb428 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/buckets/buckets.tsx
@@ -18,25 +18,9 @@
import React, { useEffect, useState } from 'react';
import moment from 'moment';
-import { Table, Tag } from 'antd';
-import {
- ColumnProps,
- ColumnsType,
- TablePaginationConfig
-} from 'antd/es/table';
-import {
- CheckCircleOutlined,
- CloseCircleOutlined,
- CloudServerOutlined,
- FileUnknownOutlined,
- HddOutlined,
- LaptopOutlined,
- SaveOutlined
-} from '@ant-design/icons';
import { ValueType } from 'react-select';
import { useLocation } from 'react-router-dom';
-import QuotaBar from '@/components/quotaBar/quotaBar';
import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel';
import AclPanel from '@/v2/components/aclDrawer/aclDrawer';
import Search from '@/v2/components/search/search';
@@ -45,20 +29,17 @@ import SingleSelect, { Option } from
'@/v2/components/select/singleSelect';
import { AutoReloadHelper } from '@/utils/autoReloadHelper';
import { AxiosGetHelper } from "@/utils/axiosRequestHelper";
-import { nullAwareLocaleCompare, showDataFetchError } from '@/utils/common';
+import { showDataFetchError } from '@/utils/common';
import { useDebounce } from '@/v2/hooks/debounce.hook';
import {
Bucket,
- BucketLayout,
- BucketLayoutTypeList,
BucketResponse,
BucketsState,
- BucketStorage,
- BucketStorageTypeList
} from '@/v2/types/bucket.types';
import './buckets.less';
+import BucketsTable, { COLUMNS } from '@/v2/components/tables/bucketsTable';
const LIMIT_OPTIONS: Option[] = [
@@ -80,38 +61,6 @@ const LIMIT_OPTIONS: Option[] = [
}
]
-const renderIsVersionEnabled = (isVersionEnabled: boolean) => {
- return isVersionEnabled
- ? <CheckCircleOutlined
- style={{ color: '#1da57a' }}
- className='icon-success' />
- : <CloseCircleOutlined className='icon-neutral' />
-};
-
-const renderStorageType = (bucketStorage: BucketStorage) => {
- const bucketStorageIconMap: Record<BucketStorage, React.ReactElement> = {
- RAM_DISK: <LaptopOutlined />,
- SSD: <SaveOutlined />,
- DISK: <HddOutlined />,
- ARCHIVE: <CloudServerOutlined />
- };
- const icon = bucketStorage in bucketStorageIconMap
- ? bucketStorageIconMap[bucketStorage]
- : <FileUnknownOutlined />;
- return <span>{icon} {bucketStorage}</span>;
-};
-
-const renderBucketLayout = (bucketLayout: BucketLayout) => {
- const bucketLayoutColorMap = {
- FILE_SYSTEM_OPTIMIZED: 'green',
- OBJECT_STORE: 'orange',
- LEGACY: 'blue'
- };
- const color = bucketLayout in bucketLayoutColorMap ?
- bucketLayoutColorMap[bucketLayout] : '';
- return <Tag color={color}>{bucketLayout}</Tag>;
-};
-
const SearchableColumnOpts = [{
label: 'Bucket',
value: 'name'
@@ -120,113 +69,6 @@ const SearchableColumnOpts = [{
value: 'volumeName'
}]
-const COLUMNS: ColumnsType<Bucket> = [
- {
- title: 'Bucket',
- dataIndex: 'name',
- key: 'name',
- sorter: (a: Bucket, b: Bucket) => a.name.localeCompare(b.name),
- defaultSortOrder: 'ascend' as const
- },
- {
- title: 'Volume',
- dataIndex: 'volumeName',
- key: 'volumeName',
- sorter: (a: Bucket, b: Bucket) => a.volumeName.localeCompare(b.volumeName),
- defaultSortOrder: 'ascend' as const
- },
- {
- title: 'Owner',
- dataIndex: 'owner',
- key: 'owner',
- sorter: (a: Bucket, b: Bucket) => nullAwareLocaleCompare(a.owner, b.owner)
- },
- {
- title: 'Versioning',
- dataIndex: 'versioning',
- key: 'isVersionEnabled',
- render: (isVersionEnabled: boolean) =>
renderIsVersionEnabled(isVersionEnabled)
- },
- {
- title: 'Storage Type',
- dataIndex: 'storageType',
- key: 'storageType',
- filterMultiple: true,
- filters: BucketStorageTypeList.map(state => ({ text: state, value: state
})),
- onFilter: (value, record: Bucket) => record.storageType === value,
- sorter: (a: Bucket, b: Bucket) =>
a.storageType.localeCompare(b.storageType),
- render: (storageType: BucketStorage) => renderStorageType(storageType)
- },
- {
- title: 'Bucket Layout',
- dataIndex: 'bucketLayout',
- key: 'bucketLayout',
- filterMultiple: true,
- filters: BucketLayoutTypeList.map(state => ({ text: state, value: state
})),
- onFilter: (value, record: Bucket) => record.bucketLayout === value,
- sorter: (a: Bucket, b: Bucket) =>
a.bucketLayout.localeCompare(b.bucketLayout),
- render: (bucketLayout: BucketLayout) => renderBucketLayout(bucketLayout)
- },
- {
- title: 'Creation Time',
- dataIndex: 'creationTime',
- key: 'creationTime',
- sorter: (a: Bucket, b: Bucket) => a.creationTime - b.creationTime,
- render: (creationTime: number) => {
- return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
- }
- },
- {
- title: 'Modification Time',
- dataIndex: 'modificationTime',
- key: 'modificationTime',
- sorter: (a: Bucket, b: Bucket) => a.modificationTime - b.modificationTime,
- render: (modificationTime: number) => {
- return modificationTime > 0 ? moment(modificationTime).format('ll LTS')
: 'NA';
- }
- },
- {
- title: 'Storage Capacity',
- key: 'quotaCapacityBytes',
- sorter: (a: Bucket, b: Bucket) => a.usedBytes - b.usedBytes,
- render: (text: string, record: Bucket) => (
- <QuotaBar
- quota={record.quotaInBytes}
- used={record.usedBytes}
- quotaType='size'
- />
- )
- },
- {
- title: 'Namespace Capacity',
- key: 'namespaceCapacity',
- sorter: (a: Bucket, b: Bucket) => a.usedNamespace - b.usedNamespace,
- render: (text: string, record: Bucket) => (
- <QuotaBar
- quota={record.quotaInNamespace}
- used={record.usedNamespace}
- quotaType='namespace'
- />
- )
- },
- {
- title: 'Source Volume',
- dataIndex: 'sourceVolume',
- key: 'sourceVolume',
- render: (sourceVolume: string) => {
- return sourceVolume ? sourceVolume : 'NA';
- }
- },
- {
- title: 'Source Bucket',
- dataIndex: 'sourceBucket',
- key: 'sourceBucket',
- render: (sourceBucket: string) => {
- return sourceBucket ? sourceBucket : 'NA';
- }
- }
-];
-
const defaultColumns = COLUMNS.map(column => ({
label: column.title as string,
value: column.key as string
@@ -291,21 +133,10 @@ const Buckets: React.FC<{}> = () => {
const debouncedSearch = useDebounce(searchTerm, 300);
const { search } = useLocation();
- const paginationConfig: TablePaginationConfig = {
- showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total}
buckets`,
- showSizeChanger: true
- };
-
function getVolumeSearchParam() {
return new URLSearchParams(search).get('volume');
};
- function getFilteredData(data: Bucket[]) {
- return data.filter(
- (bucket: Bucket) => bucket[searchColumn].includes(debouncedSearch)
- );
- }
-
function handleVolumeChange(selected: ValueType<Option, true>) {
const { volumeBucketMap } = state;
const volumeSelections = (selected as Option[]);
@@ -327,50 +158,6 @@ const Buckets: React.FC<{}> = () => {
setShowPanel(true);
}
- function filterSelectedColumns() {
- const columnKeys = selectedColumns.map((column) => column.value);
- return COLUMNS.filter(
- (column) => columnKeys.indexOf(column.key as string) >= 0
- )
- }
-
- function addAclColumn() {
- // Inside the class component to access the React internal state
- const aclLinkColumn: ColumnProps<Bucket> = {
- title: 'ACLs',
- dataIndex: 'acls',
- key: 'acls',
- render: (_: any, record: Bucket) => {
- return (
- <a
- key='acl'
- onClick={() => {
- handleAclLinkClick(record);
- }}
- >
- Show ACL
- </a>
- );
- }
- };
-
- if (COLUMNS.length > 0 && COLUMNS[COLUMNS.length - 1].key !== 'acls') {
- // Push the ACL column for initial
- COLUMNS.push(aclLinkColumn);
- } else {
- // Replace old ACL column with new ACL column with correct reference
- // e.g. After page is reloaded / redirect from other page
- COLUMNS[COLUMNS.length - 1] = aclLinkColumn;
- }
-
- if (defaultColumns.length > 0 && defaultColumns[defaultColumns.length -
1].label !== 'acls') {
- defaultColumns.push({
- label: aclLinkColumn.title as string,
- value: aclLinkColumn.key as string
- });
- }
- };
-
function handleColumnChange(selected: ValueType<Option, true>) {
setSelectedColumns(selected as Option[]);
}
@@ -447,7 +234,6 @@ const Buckets: React.FC<{}> = () => {
useEffect(() => {
autoReloadHelper.startPolling();
- addAclColumn();
const initialVolume = getVolumeSearchParam();
if (initialVolume) {
setSelectedVolumes([{
@@ -537,17 +323,13 @@ const Buckets: React.FC<{}> = () => {
setSearchColumn(value as 'name' | 'volumeName');
}} />
</div>
- <div>
- <Table
- dataSource={getFilteredData(bucketsUnderVolume)}
- columns={filterSelectedColumns()}
- loading={loading}
- rowKey='volume'
- pagination={paginationConfig}
- scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
- locale={{ filterTitle: '' }}
- />
- </div>
+ <BucketsTable
+ loading={loading}
+ data={bucketsUnderVolume}
+ handleAclClick={handleAclLinkClick}
+ selectedColumns={selectedColumns}
+ searchColumn={searchColumn}
+ searchTerm={debouncedSearch} />
</div>
<AclPanel
visible={showPanel}
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.less
similarity index 59%
copy from
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
copy to
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.less
index 67f007706a..a2fb93f7da 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.less
@@ -16,29 +16,33 @@
* limitations under the License.
*/
-import { Acl } from "@/v2/types/acl.types";
-import { Option } from "@/v2/components/select/multiSelect";
+.content-div {
+ min-height: unset;
-export type Volume = {
- volume: string;
- owner: string;
- admin: string;
- creationTime: number;
- modificationTime: number;
- quotaInBytes: number;
- quotaInNamespace: number;
- usedNamespace: number;
- acls?: Acl[];
-}
+ .table-header-section {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
-export type VolumesResponse = {
- totalCount: number;
- volumes: Volume[];
-}
+ .table-filter-section {
+ font-size: 14px;
+ font-weight: normal;
+ display: flex;
+ column-gap: 8px;
+ padding: 16px 8px;
+ }
+ }
+
+ .uuid-tooltip {
+ cursor: pointer;
+ .ant-tooltip-inner {
+ width: max-content;
+ }
+ }
-export type VolumesState = {
- data: Volume[];
- lastUpdated: number;
- columnOptions: Option[];
- currentRow: Volume | Record<string, never>;
+ .tag-block {
+ display: flex;
+ column-gap: 8px;
+ padding: 0px 8px 16px 8px;
+ }
}
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.tsx
new file mode 100644
index 0000000000..9059da91f9
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/pipelines/pipelines.tsx
@@ -0,0 +1,160 @@
+/*
+ * 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, {
+ useEffect,
+ useRef,
+ useState
+} from 'react';
+import moment from 'moment';
+import { ValueType } from 'react-select';
+
+import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel';
+import Search from '@/v2/components/search/search';
+import MultiSelect, { Option } from '@/v2/components/select/multiSelect';
+import PipelinesTable, { COLUMNS } from
'@/v2/components/tables/pipelinesTable';
+import { showDataFetchError } from '@/utils/common';
+import { AutoReloadHelper } from '@/utils/autoReloadHelper';
+import { AxiosGetHelper, cancelRequests } from '@/utils/axiosRequestHelper';
+import { useDebounce } from '@/v2/hooks/debounce.hook';
+
+import {
+ Pipeline,
+ PipelinesResponse,
+ PipelinesState
+} from '@/v2/types/pipelines.types';
+
+import './pipelines.less';
+
+
+const defaultColumns = COLUMNS.map(column => ({
+ label: (typeof column.title === 'string')
+ ? column.title
+ : (column.title as Function)().props.children[0],
+ value: column.key as string,
+}));
+
+const Pipelines: React.FC<{}> = () => {
+ const cancelSignal = useRef<AbortController>();
+
+ const [state, setState] = useState<PipelinesState>({
+ activeDataSource: [],
+ columnOptions: defaultColumns,
+ lastUpdated: 0,
+ });
+ const [loading, setLoading] = useState<boolean>(false);
+ const [selectedColumns, setSelectedColumns] =
useState<Option[]>(defaultColumns);
+ const [searchTerm, setSearchTerm] = useState<string>('');
+
+ const debouncedSearch = useDebounce(searchTerm, 300);
+
+ const loadData = () => {
+ setLoading(true);
+ //Cancel any previous requests
+ cancelRequests([cancelSignal.current!]);
+
+ const { request, controller } = AxiosGetHelper(
+ '/api/v1/pipelines',
+ cancelSignal.current
+ );
+
+ cancelSignal.current = controller;
+ request.then(response => {
+ const pipelinesResponse: PipelinesResponse = response.data;
+ const pipelines: Pipeline[] = pipelinesResponse?.pipelines ?? {};
+ setState({
+ ...state,
+ activeDataSource: pipelines,
+ lastUpdated: Number(moment())
+ })
+ setLoading(false);
+ }).catch(error => {
+ setLoading(false);
+ showDataFetchError(error.toString());
+ })
+ }
+
+ const autoReloadHelper: AutoReloadHelper = new AutoReloadHelper(loadData);
+
+ useEffect(() => {
+ autoReloadHelper.startPolling();
+ loadData();
+ return (() => {
+ autoReloadHelper.stopPolling();
+ cancelRequests([cancelSignal.current!]);
+ })
+ }, []);
+
+ function handleColumnChange(selected: ValueType<Option, true>) {
+ setSelectedColumns(selected as Option[]);
+ }
+
+ const {
+ activeDataSource,
+ columnOptions,
+ lastUpdated
+ } = state;
+
+ return (
+ <>
+ <div className='page-header-v2'>
+ Pipelines
+ <AutoReloadPanel
+ isLoading={loading}
+ lastRefreshed={lastUpdated}
+ togglePolling={autoReloadHelper.handleAutoReloadToggle}
+ onReload={loadData} />
+ </div>
+ <div style={{ padding: '24px' }}>
+ <div className='content-div'>
+ <div className='table-header-section'>
+ <div className='table-filter-section'>
+ <MultiSelect
+ options={columnOptions}
+ defaultValue={selectedColumns}
+ selected={selectedColumns}
+ placeholder='Columns'
+ onChange={handleColumnChange}
+ onTagClose={() => { }}
+ fixedColumn='pipelineId'
+ columnLength={COLUMNS.length} />
+ </div>
+ <Search
+ disabled={activeDataSource?.length < 1}
+ searchOptions={[{
+ label: 'Pipeline ID',
+ value: 'pipelineId'
+ }]}
+ searchInput={searchTerm}
+ searchColumn={'pipelineId'}
+ onSearchChange={
+ (e: React.ChangeEvent<HTMLInputElement>) =>
setSearchTerm(e.target.value)
+ }
+ onChange={() => { }} />
+ </div>
+ <PipelinesTable
+ loading={loading}
+ data={activeDataSource}
+ selectedColumns={selectedColumns}
+ searchTerm={debouncedSearch} />
+ </div>
+ </div>
+ </>
+ );
+}
+export default Pipelines;
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx
index 605883caff..cb25cedbce 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx
@@ -18,22 +18,16 @@
import React, { useEffect, useRef, useState } from 'react';
import moment from 'moment';
-import { Table } from 'antd';
-import { Link } from 'react-router-dom';
-import {
- TablePaginationConfig,
- ColumnsType
-} from 'antd/es/table';
import { ValueType } from 'react-select/src/types';
-import QuotaBar from '@/components/quotaBar/quotaBar';
import AclPanel from '@/v2/components/aclDrawer/aclDrawer';
import AutoReloadPanel from '@/components/autoReloadPanel/autoReloadPanel';
-import MultiSelect, { Option } from '@/v2/components/select/multiSelect';
import SingleSelect from '@/v2/components/select/singleSelect';
+import MultiSelect, { Option } from '@/v2/components/select/multiSelect';
+import VolumesTable, { COLUMNS } from '@/v2/components/tables/volumesTable';
import Search from '@/v2/components/search/search';
-import { byteToSize, showDataFetchError } from '@/utils/common';
+import { showDataFetchError } from '@/utils/common';
import { AutoReloadHelper } from '@/utils/autoReloadHelper';
import { AxiosGetHelper, cancelRequests } from "@/utils/axiosRequestHelper";
import { useDebounce } from '@/v2/hooks/debounce.hook';
@@ -72,93 +66,6 @@ const Volumes: React.FC<{}> = () => {
const cancelSignal = useRef<AbortController>();
- const COLUMNS: ColumnsType<Volume> = [
- {
- title: 'Volume',
- dataIndex: 'volume',
- key: 'volume',
- sorter: (a: Volume, b: Volume) => a.volume.localeCompare(b.volume),
- defaultSortOrder: 'ascend' as const,
- width: '15%'
- },
- {
- title: 'Owner',
- dataIndex: 'owner',
- key: 'owner',
- sorter: (a: Volume, b: Volume) => a.owner.localeCompare(b.owner)
- },
- {
- title: 'Admin',
- dataIndex: 'admin',
- key: 'admin',
- sorter: (a: Volume, b: Volume) => a.admin.localeCompare(b.admin)
- },
- {
- title: 'Creation Time',
- dataIndex: 'creationTime',
- key: 'creationTime',
- sorter: (a: Volume, b: Volume) => a.creationTime - b.creationTime,
- render: (creationTime: number) => {
- return creationTime > 0 ? moment(creationTime).format('ll LTS') : 'NA';
- }
- },
- {
- title: 'Modification Time',
- dataIndex: 'modificationTime',
- key: 'modificationTime',
- sorter: (a: Volume, b: Volume) => a.modificationTime -
b.modificationTime,
- render: (modificationTime: number) => {
- return modificationTime > 0 ? moment(modificationTime).format('ll
LTS') : 'NA';
- }
- },
- {
- title: 'Quota (Size)',
- dataIndex: 'quotaInBytes',
- key: 'quotaInBytes',
- render: (quotaInBytes: number) => {
- return quotaInBytes && quotaInBytes !== -1 ? byteToSize(quotaInBytes,
3) : 'NA';
- }
- },
- {
- title: 'Namespace Capacity',
- key: 'namespaceCapacity',
- sorter: (a: Volume, b: Volume) => a.usedNamespace - b.usedNamespace,
- render: (text: string, record: Volume) => (
- <QuotaBar
- quota={record.quotaInNamespace}
- used={record.usedNamespace}
- quotaType='namespace'
- />
- )
- },
- {
- title: 'Actions',
- key: 'actions',
- render: (_: any, record: Volume) => {
- const searchParams = new URLSearchParams();
- searchParams.append('volume', record.volume);
-
- return (
- <>
- <Link
- key="listBuckets"
- to={`/Buckets?${searchParams.toString()}`}
- style={{
- marginRight: '16px'
- }}>
- Show buckets
- </Link>
- <a
- key='acl'
- onClick={() => handleAclLinkClick(record)}>
- Show ACL
- </a>
- </>
- );
- }
- }
- ];
-
const defaultColumns = COLUMNS.map(column => ({
label: column.title as string,
value: column.key as string,
@@ -167,10 +74,10 @@ const Volumes: React.FC<{}> = () => {
const [state, setState] = useState<VolumesState>({
data: [],
lastUpdated: 0,
- columnOptions: defaultColumns,
- currentRow: {}
+ columnOptions: defaultColumns
});
const [loading, setLoading] = useState<boolean>(false);
+ const [currentRow, setCurrentRow] = useState<Volume | Record<string,
never>>({});
const [selectedColumns, setSelectedColumns] =
useState<Option[]>(defaultColumns);
const [selectedLimit, setSelectedLimit] = useState<Option>(LIMIT_OPTIONS[0]);
const [searchColumn, setSearchColumn] = useState<'volume' | 'owner' |
'admin'>('volume');
@@ -255,35 +162,13 @@ const Volumes: React.FC<{}> = () => {
function handleAclLinkClick(volume: Volume) {
- setState({
- ...state,
- currentRow: volume
- });
+ setCurrentRow(volume);
setShowPanel(true);
}
- function filterSelectedColumns() {
- const columnKeys = selectedColumns.map((column) => column.value);
- return COLUMNS.filter(
- (column) => columnKeys.indexOf(column.key as string) >= 0
- )
- }
-
- function getFilteredData(data: Volume[]) {
- return data.filter(
- (volume: Volume) => volume[searchColumn].includes(debouncedSearch)
- );
- }
-
-
- const paginationConfig: TablePaginationConfig = {
- showTotal: (total: number, range) => `${range[0]}-${range[1]} of ${total}
volumes`,
- showSizeChanger: true
- };
-
const {
data, lastUpdated,
- columnOptions, currentRow
+ columnOptions
} = state;
return (
@@ -329,17 +214,13 @@ const Volumes: React.FC<{}> = () => {
setSearchColumn(value as 'volume' | 'owner' | 'admin');
}} />
</div>
- <div>
- <Table
- dataSource={getFilteredData(data)}
- columns={filterSelectedColumns()}
- loading={loading}
- rowKey='volume'
- pagination={paginationConfig}
- scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
- locale={{ filterTitle: '' }}
- />
- </div>
+ <VolumesTable
+ loading={loading}
+ data={data}
+ handleAclClick={handleAclLinkClick}
+ selectedColumns={selectedColumns}
+ searchColumn={searchColumn}
+ searchTerm={debouncedSearch} />
</div>
<AclPanel
visible={showPanel}
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
index 8a37ef9c51..37ec3964bb 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
@@ -20,6 +20,7 @@ import { lazy } from 'react';
const Overview = lazy(() => import('@/v2/pages/overview/overview'));
const Volumes = lazy(() => import('@/v2/pages/volumes/volumes'))
const Buckets = lazy(() => import('@/v2/pages/buckets/buckets'));
+const Pipelines = lazy(() => import('@/v2/pages/pipelines/pipelines'));
export const routesV2 = [
{
@@ -33,5 +34,9 @@ export const routesV2 = [
{
path: '/Buckets',
component: Buckets
+ },
+ {
+ path: '/Pipelines',
+ component: Pipelines
}
];
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts
index 5cfc89d85e..eb499dc617 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts
@@ -17,7 +17,6 @@
*/
import { Acl } from "@/v2/types/acl.types";
-import { Option } from "@/v2/components/select/singleSelect";
import { Option as MultiOption } from "@/v2/components/select/multiSelect";
// Corresponds to OzoneManagerProtocolProtos.StorageTypeProto
@@ -68,4 +67,13 @@ export type BucketsState = {
volumeBucketMap: Map<string, Set<Bucket>>;
bucketsUnderVolume: Bucket[];
volumeOptions: MultiOption[];
-}
\ No newline at end of file
+}
+
+export type BucketsTableProps = {
+ loading: boolean;
+ data: Bucket[];
+ handleAclClick: (arg0: Bucket) => void;
+ selectedColumns: MultiOption[];
+ searchColumn: 'name' | 'volumeName';
+ searchTerm: string;
+}
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts
new file mode 100644
index 0000000000..7c5a23bc0a
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/pipelines.types.ts
@@ -0,0 +1,62 @@
+/*
+ * 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 { Option } from "@/v2/components/select/multiSelect";
+
+export const PipelineStatusList = [
+ 'OPEN',
+ 'CLOSING',
+ 'QUASI_CLOSED',
+ 'CLOSED',
+ 'UNHEALTHY',
+ 'INVALID',
+ 'DELETED',
+ 'DORMANT'
+] as const;
+export type PipelineStatus = typeof PipelineStatusList[number];
+
+export type Pipeline = {
+ pipelineId: string;
+ status: PipelineStatus;
+ replicationType: string;
+ leaderNode: string;
+ datanodes: string[];
+ lastLeaderElection: number;
+ duration: number;
+ leaderElections: number;
+ replicationFactor: string;
+ containers: number;
+}
+
+export type PipelinesResponse = {
+ totalCount: number;
+ pipelines: Pipeline[];
+}
+
+export type PipelinesState = {
+ activeDataSource: Pipeline[];
+ columnOptions: Option[];
+ lastUpdated: number;
+}
+
+export type PipelinesTableProps = {
+ loading: boolean;
+ data: Pipeline[];
+ selectedColumns: Option[];
+ searchTerm: string;
+}
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
index 67f007706a..b808d40358 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
@@ -40,5 +40,13 @@ export type VolumesState = {
data: Volume[];
lastUpdated: number;
columnOptions: Option[];
- currentRow: Volume | Record<string, never>;
+}
+
+export type VolumesTableProps = {
+ loading: boolean;
+ data: Volume[];
+ handleAclClick: (arg0: Volume) => void;
+ selectedColumns: Option[];
+ searchColumn: 'volume' | 'owner' | 'admin';
+ searchTerm: string;
}
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/utils/momentUtils.ts
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/utils/momentUtils.ts
new file mode 100644
index 0000000000..fb553d0db3
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/utils/momentUtils.ts
@@ -0,0 +1,63 @@
+/*
+ * 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 moment from "moment";
+
+moment.updateLocale('en', {
+ relativeTime: {
+ past: '%s ago',
+ s: '%ds',
+ m: '1min',
+ mm: '%dmins',
+ h: '1hr',
+ hh: '%dhrs',
+ d: '1d',
+ dd: '%dd',
+ M: '1m',
+ MM: '%dm',
+ y: '1y',
+ yy: '%dy'
+ }
+});
+
+export function getTimeDiffFromTimestamp(timestamp: number): string {
+ const timestampDate = new Date(timestamp);
+ return moment(timestampDate).fromNow();
+}
+
+export function getDurationFromTimestamp(timestamp: number): string {
+ const duration: moment.Duration = moment.duration(timestamp, 'milliseconds');
+ // return nothing when the duration is falsy or not correctly parsed (P0D)
+ if(!duration || duration.toISOString() === "P0D") return '';
+
+ let elapsedTime = [];
+ const durationBreakdowns: Record<string, number> = {
+ 'y': Math.floor(duration.years()),
+ 'm': Math.floor(duration.months()),
+ 'd': Math.floor(duration.days()),
+ 'h': Math.floor(duration.hours()),
+ 'min': Math.floor(duration.minutes()),
+ 's': Math.floor(duration.seconds())
+ }
+
+ for (const [key, value] of Object.entries(durationBreakdowns)) {
+ value > 0 && elapsedTime.push(value + key);
+ }
+
+ return (elapsedTime.length === 0) ? 'Just now' : elapsedTime.join(' ');
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]