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 ebad350033 HDDS-11155. Improve Volumes page UI (#7048)
ebad350033 is described below
commit ebad3500332fc2639615b36dde2680bdb3c5d5ca
Author: Abhishek Pal <[email protected]>
AuthorDate: Tue Aug 20 10:50:22 2024 +0530
HDDS-11155. Improve Volumes page UI (#7048)
---
.../webapps/recon/ozone-recon-web/src/app.tsx | 12 +-
.../recon/ozone-recon-web/src/utils/common.tsx | 5 +-
.../src/v2/components/aclDrawer/aclDrawer.tsx | 119 +++++++
.../src/v2/components/eChart/eChart.tsx | 2 +-
.../loader/loader.tsx} | 28 +-
.../src/v2/components/search/search.tsx | 70 ++++
.../src/v2/components/select/columnTag.tsx | 67 ++++
.../src/v2/components/select/multiSelect.tsx | 104 ++++++
.../src/v2/components/select/singleSelect.tsx | 87 +++++
.../{routes-v2.tsx => constants/acl.constants.tsx} | 25 +-
.../src/v2/constants/select.constants.tsx | 62 ++++
.../v2/{routes-v2.tsx => hooks/debounce.hook.tsx} | 23 +-
.../src/v2/pages/volumes/volumes.less | 41 +++
.../src/v2/pages/volumes/volumes.tsx | 353 +++++++++++++++++++++
.../recon/ozone-recon-web/src/v2/routes-v2.tsx | 10 +-
.../src/v2/{routes-v2.tsx => types/acl.types.ts} | 35 +-
.../ozone-recon-web/src/v2/types/bucket.types.ts | 55 ++++
.../ozone-recon-web/src/v2/types/volume.types.ts | 44 +++
18 files changed, 1104 insertions(+), 38 deletions(-)
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.tsx
index c52fe9efa9..0ad6aa3f17 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/app.tsx
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import React from 'react';
+import React, { Suspense } from 'react';
import { Switch as AntDSwitch, Layout } from 'antd';
import NavBar from './components/navBar/navBar';
@@ -27,6 +27,8 @@ import { routesV2 } from '@/v2/routes-v2';
import { MakeRouteWithSubRoutes } from '@/makeRouteWithSubRoutes';
import classNames from 'classnames';
+import Loader from '@/v2/components/loader/loader';
+
import './app.less';
const {
@@ -80,9 +82,11 @@ class App extends React.Component<Record<string, object>,
IAppState> {
<Redirect to='/Overview' />
</Route>
{(enableNewUI)
- ? routesV2.map(
- (route, index) => <MakeRouteWithSubRoutes key={index}
{...route} />
- )
+ ? <Suspense fallback={<Loader/>}>
+ {routesV2.map(
+ (route, index) => <MakeRouteWithSubRoutes key={index}
{...route} />
+ )}
+ </Suspense>
: routes.map(
(route, index) => <MakeRouteWithSubRoutes key={index}
{...route} />
)
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/common.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/common.tsx
index 6886fd189f..f641b8797d 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/common.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/utils/common.tsx
@@ -44,9 +44,8 @@ const showInfoNotification = (title: string, description:
string) => {
export const showDataFetchError = (error: string) => {
let title = 'Error while fetching data';
- if (error.includes('CanceledError')) {
- error = 'Previous request cancelled because context changed'
- }
+
+ if (error.includes('CanceledError')) return;
if (error.includes('metadata')) {
title = 'Metadata Initialization:';
showInfoNotification(title, error);
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/aclDrawer/aclDrawer.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/aclDrawer/aclDrawer.tsx
new file mode 100644
index 0000000000..af0931c17f
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/aclDrawer/aclDrawer.tsx
@@ -0,0 +1,119 @@
+/*
+ * 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, useState } from 'react';
+import { Table, Drawer, Tag } from 'antd';
+
+import { AclRightsColorMap, AclIdColorMap } from
'@/v2/constants/acl.constants';
+import { Acl, ACLIdentity, ACLIdentityTypeList } from '@/v2/types/acl.types';
+import { ColumnType } from 'antd/es/table';
+
+// ------------- Types -------------- //
+type AclDrawerProps = {
+ visible: boolean;
+ acls: Acl[] | undefined;
+ entityName: string;
+ entityType: string;
+ onClose: () => void;
+}
+
+
+// ------------- Component -------------- //
+const AclPanel: React.FC<AclDrawerProps> = ({
+ visible,
+ acls,
+ entityType,
+ entityName,
+ onClose
+}) => {
+ const [isVisible, setIsVisible] = useState<boolean>(false);
+
+ useEffect(() => {
+ setIsVisible(visible);
+ }, [visible]);
+
+ const renderAclList = (_: string, acl: Acl) => {
+ return acl.aclList.map(aclRight => (
+ <Tag key={aclRight} color={AclRightsColorMap[aclRight as keyof typeof
AclRightsColorMap]}>
+ {aclRight}
+ </Tag>
+ ))
+ }
+
+ const renderAclIdentityType = (acl: string) => {
+ return (
+ <Tag color={AclIdColorMap[acl as keyof typeof AclIdColorMap]}>
+ {acl}
+ </Tag>
+ )
+ }
+
+ const COLUMNS: ColumnType<Acl>[] = [
+ {
+ title: 'Name',
+ dataIndex: 'name',
+ key: 'name',
+ sorter: (a: Acl, b: Acl) => a.name.localeCompare(b.name),
+ },
+ {
+ title: 'ACL Type',
+ dataIndex: 'type',
+ key: 'type',
+ filterMultiple: true,
+ filters: ACLIdentityTypeList.map(state => ({ text: state, value: state
})),
+ onFilter: (value: ACLIdentity, record: Acl) => (record.type === value),
+ sorter: (a: Acl, b: Acl) => a.type.localeCompare(b.type),
+ render: renderAclIdentityType
+ },
+ {
+ title: 'ACL Scope',
+ dataIndex: 'scope',
+ key: 'scope',
+ },
+ {
+ title: 'ACLs',
+ dataIndex: 'aclList',
+ key: 'acls',
+ render: renderAclList
+ }
+ ];
+
+ return (
+ <div className='site-drawer-render-in-current-wrapper'>
+ <Drawer
+ title={`ACL for ${entityType} ${entityName}`}
+ placement='right'
+ width='40%'
+ closable={true}
+ visible={isVisible}
+ getContainer={false}
+ style={{ position: 'absolute' }}
+ onClose={onClose}
+ >
+ <Table
+ dataSource={acls}
+ rowKey='name'
+ locale={{ filterTitle: '' }}
+ columns={COLUMNS}>
+ </Table>
+ </Drawer>
+ </div>
+ );
+};
+
+export default AclPanel;
\ No newline at end of file
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/eChart/eChart.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/eChart/eChart.tsx
index 8be22fcc9f..79fa076033 100644
---
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/eChart/eChart.tsx
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/eChart/eChart.tsx
@@ -83,7 +83,7 @@ const EChart = ({
}
}, [loading, theme]); // If we switch theme we should put chart in loading
mode, and also if loading changes i.e completes then hide loader
- return <div ref={chartRef} style={{ width: "100em", height: "50em", margin:
'auto', ...style }} />;
+ return <div ref={chartRef} style={{ width: "50vw", height: "25vh", margin:
'auto', ...style }} />;
}
export default EChart;
\ No newline at end of file
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/components/loader/loader.tsx
similarity index 60%
copy from
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
copy to
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/loader/loader.tsx
index 4cdd700d50..b05eaa5f0a 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/components/loader/loader.tsx
@@ -16,11 +16,25 @@
* limitations under the License.
*/
-import Overview from '@/v2/pages/overview/overview';
+import React from "react"
+import { Spin } from "antd"
+import { LoadingOutlined } from "@ant-design/icons"
-export const routesV2: IRoute[] = [
- {
- path: '/Overview',
- component: Overview
- }
-];
+// ------------- Constants -------------- //
+const loaderStyle: React.CSSProperties = {
+ height: '100%',
+ width: '100%',
+ textAlign: 'center',
+ paddingTop: '25%'
+}
+
+// ------------- Component -------------- //
+const Loader: React.FC = () => {
+ return (
+ <div style={loaderStyle}>
+ <Spin indicator={<LoadingOutlined style={{ color: '#1AA57A', fontSize:
48}} spin/>}/>
+ </div>
+ )
+}
+
+export default Loader;
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
new file mode 100644
index 0000000000..21d4341787
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
@@ -0,0 +1,70 @@
+/*
+ * 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 { Input, Select } from 'antd';
+
+import { Option } from '@/v2/components/select/singleSelect';
+
+// ------------- Types -------------- //
+type SearchProps = {
+ searchColumn?: string;
+ searchInput: string;
+ searchOptions?: Option[];
+ onSearchChange: (
+ arg0: React.ChangeEvent<HTMLInputElement>
+ ) => void;
+ onChange: (
+ value: string,
+ //OptionType, OptionGroupData and OptionData are not
+ //currently exported by AntD hence set to any
+ option: any
+ ) => void;
+}
+
+// ------------- Component -------------- //
+const Search: React.FC<SearchProps> = ({
+ searchColumn,
+ searchInput = '',
+ searchOptions = [],
+ onSearchChange = () => {},
+ onChange = () => {} // Assign default value as a void function
+}) => {
+
+ const selectFilter = searchColumn
+ ? (<Select
+ defaultValue={searchColumn}
+ options={searchOptions}
+ onChange={onChange} />)
+ : null
+
+ return (
+ <Input
+ placeholder='Enter Search text'
+ allowClear={true}
+ value={searchInput}
+ addonBefore={selectFilter}
+ onChange={onSearchChange}
+ size='middle'
+ style={{
+ maxWidth: 400
+ }}/>
+ )
+}
+
+export default Search;
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/select/columnTag.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/select/columnTag.tsx
new file mode 100644
index 0000000000..f367504286
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/select/columnTag.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 { Tag } from "antd";
+import { createPortal } from "react-dom";
+
+
+// ------------- Types -------------- //
+/**
+ * Due to design decisions we are currently not using the Tags
+ * Until we reach a concensus on a better way to display the filter
+ * Keeping the code in case we require it in the future
+ */
+export type TagProps = {
+ label: string;
+ closable: boolean;
+ tagRef: React.RefObject<HTMLDivElement>;
+ onClose: (arg0: string) => void;
+}
+
+// ------------- Component -------------- //
+const ColumnTag: React.FC<TagProps> = ({
+ label = '',
+ closable = true,
+ tagRef = null,
+ onClose = () => {} // Assign default value as void funciton
+}) => {
+ const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
+ // By default when clickin on the tags the text will get selected
+ // which might interfere with user experience as people would want to
close tags
+ // but accidentally select tag text. Hence we prevent this behaviour.
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
+ if (!tagRef?.current) return null;
+
+ return createPortal(
+ <Tag
+ key={label}
+ onMouseDown={onPreventMouseDown}
+ closable={closable}
+ onClose={() => (onClose(label))}
+ style={{marginRight: 3}}>
+ {label}
+ </Tag>,
+ tagRef.current
+ );
+}
+
+export default ColumnTag;
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/select/multiSelect.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/select/multiSelect.tsx
new file mode 100644
index 0000000000..7a6b494aae
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/select/multiSelect.tsx
@@ -0,0 +1,104 @@
+/*
+ * 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 {
+ default as ReactSelect,
+ Props as ReactSelectProps,
+ components,
+ OptionProps,
+ ValueType
+} from 'react-select';
+
+import { selectStyles } from "@/v2/constants/select.constants";
+
+
+// ------------- Types -------------- //
+export type Option = {
+ label: string;
+ value: string;
+}
+
+interface MultiSelectProps extends ReactSelectProps<Option, true> {
+ options: Option[];
+ selected: Option[];
+ placeholder: string;
+ fixedColumn: string;
+ columnLength: number;
+ onChange: (arg0: ValueType<Option, true>) => void;
+ onTagClose: (arg0: string) => void;
+}
+
+// ------------- Component -------------- //
+const MultiSelect: React.FC<MultiSelectProps> = ({
+ options = [],
+ selected = [],
+ maxSelected = 5,
+ placeholder = 'Columns',
+ fixedColumn,
+ columnLength,
+ tagRef,
+ onTagClose = () => { }, // Assign default value as a void function
+ onChange = () => { }, // Assign default value as a void function
+ ...props
+}) => {
+
+ const Option: React.FC<OptionProps<Option, true>> = (props) => {
+ return (
+ <div>
+ <components.Option
+ {...props}>
+ <input
+ type='checkbox'
+ checked={props.isSelected}
+ style={{
+ marginRight: '8px',
+ accentColor: '#1AA57A'
+ }}
+ onChange={() => null} />
+ <label>{props.label}</label>
+ </components.Option>
+ </div>
+ )
+ }
+
+ return (
+ <ReactSelect
+ {...props}
+ isMulti={true}
+ closeMenuOnSelect={false}
+ hideSelectedOptions={false}
+ isClearable={false}
+ isSearchable={false}
+ controlShouldRenderValue={false}
+ classNamePrefix='multi-select'
+ options={options}
+ components={{
+ Option
+ }}
+ placeholder={placeholder}
+ value={selected}
+ onChange={(selected: ValueType<Option, true>) => {
+ if (selected?.length === options.length) return onChange!(options);
+ return onChange!(selected);
+ }}
+ styles={selectStyles} />
+ )
+}
+
+export default MultiSelect;
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/select/singleSelect.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/select/singleSelect.tsx
new file mode 100644
index 0000000000..41ab03f598
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/select/singleSelect.tsx
@@ -0,0 +1,87 @@
+/*
+ * 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 Select, {
+ Props as ReactSelectProps,
+ components,
+ ValueType,
+ ValueContainerProps,
+ StylesConfig
+} from 'react-select';
+
+import { selectStyles } from "@/v2/constants/select.constants";
+
+
+// ------------- Types -------------- //
+export type Option = {
+ label: string;
+ value: string;
+}
+
+interface SingleSelectProps extends ReactSelectProps<Option, false> {
+ options: Option[];
+ placeholder: string;
+ onChange: (arg0: ValueType<Option, false>) => void;
+}
+
+// ------------- Component -------------- //
+const SingleSelect: React.FC<SingleSelectProps> = ({
+ options = [],
+ placeholder = 'Limit',
+ onChange = () => { }, // Assign default value as a void function
+ ...props // Desctructure other select props
+}) => {
+
+
+ const ValueContainer = ({ children, ...props }: ValueContainerProps<Option,
false>) => {
+ const selectedLimit = props.getValue() as Option[];
+ return (
+ <components.ValueContainer {...props}>
+ {React.Children.map(children, (child) => (
+ ((child as React.ReactElement<any, string
+ | React.JSXElementConstructor<any>>
+ | React.ReactPortal)?.type as
React.JSXElementConstructor<any>)).name === "DummyInput"
+ ? child
+ : null
+ )}
+ Limit: {selectedLimit[0]?.label ?? ''}
+ </components.ValueContainer>
+ );
+ };
+
+ return (
+ <Select
+ {...props}
+ isClearable={false}
+ closeMenuOnSelect={true}
+ classNamePrefix='single-select'
+ isSearchable={false}
+ options={options}
+ components={{
+ ValueContainer
+ }}
+ placeholder={placeholder}
+ onChange={(selected: ValueType<Option, false>) => {
+ return onChange!(selected);
+ }}
+ styles={selectStyles as StylesConfig<Option, false>} />
+ );
+}
+
+export default SingleSelect;
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/constants/acl.constants.tsx
similarity index 71%
copy from
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
copy to
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/acl.constants.tsx
index 4cdd700d50..d1cc54dab2 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/constants/acl.constants.tsx
@@ -16,11 +16,22 @@
* limitations under the License.
*/
-import Overview from '@/v2/pages/overview/overview';
+export const AclIdColorMap = {
+ USER: 'green',
+ GROUP: 'blue',
+ WORLD: 'magenta',
+ ANONYMOUS: 'gray',
+ CLIENT_IP: 'gold'
+};
-export const routesV2: IRoute[] = [
- {
- path: '/Overview',
- component: Overview
- }
-];
+export const AclRightsColorMap = {
+ READ: 'green',
+ WRITE: 'blue',
+ CREATE: 'orange',
+ LIST: 'magenta',
+ DELETE: 'red',
+ READ_ACL: 'lime',
+ WRITE_ACL: 'purple',
+ ALL: 'gold',
+ NONE: 'gray'
+};
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/select.constants.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/select.constants.tsx
new file mode 100644
index 0000000000..465c2533bc
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/constants/select.constants.tsx
@@ -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 { StylesConfig } from "react-select";
+import { Option } from "@/v2/components/select/multiSelect";
+
+export const selectStyles: StylesConfig<Option, true> = {
+ control: (baseStyles, state) => ({
+ ...baseStyles,
+ minWidth: 200,
+ boxShadow: 'none',
+ borderRadius: '2px',
+ borderColor: state.isFocused ? '#1AA57A' : '#E6E6E6',
+ '&:hover': {
+ borderColor: '#1AA57A'
+ }
+ }),
+ option: (baseStyles, state) => ({
+ ...baseStyles,
+ display: 'flex',
+ padding: '5px 12px',
+ alignItems: 'center',
+ color: state.isSelected ? '#1AA57A' : '#262626',
+ backgroundColor: state.isSelected ? '#EDF7F4' : '#FFFFFF',
+ '&:active': {
+ color: state.isSelected ? '#FFFFFF' : '#262626',
+ backgroundColor: state.isSelected ? '#64BDA1' : '#EDF7F4'
+ }
+ }),
+ menuList: (baseStyles) => ({
+ ...baseStyles,
+ boxShadow: 'rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3)
0px 3px 7px -3px',
+ padding: 0
+ }),
+ menu: (baseStyles) => ({
+ ...baseStyles,
+ height: 100
+ }),
+ placeholder: (baseStyles) => ({
+ ...baseStyles,
+ color: 'rgba(0, 0, 0, 0.85)'
+
+ }),
+ indicatorSeparator: () => ({
+ display: 'none'
+ })
+}
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/hooks/debounce.hook.tsx
similarity index 63%
copy from
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
copy to
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/hooks/debounce.hook.tsx
index 4cdd700d50..e66dbd5679 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/hooks/debounce.hook.tsx
@@ -16,11 +16,20 @@
* limitations under the License.
*/
-import Overview from '@/v2/pages/overview/overview';
+import React from 'react';
-export const routesV2: IRoute[] = [
- {
- path: '/Overview',
- component: Overview
- }
-];
+export function useDebounce<T>(value: T, timeout: number): T {
+ const [debounceValue, setDebounceValue] = React.useState<T>(value);
+
+ React.useEffect(() => {
+ const timeoutHandler = setTimeout(() => {
+ setDebounceValue(value);
+ }, timeout);
+
+ return () => {
+ clearTimeout(timeoutHandler);
+ }
+ }, [value, timeout]); // Need to set new timeout anytime the value or
timeout duration changes
+
+ return debounceValue;
+}
diff --git
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.less
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.less
new file mode 100644
index 0000000000..8f4c8ffaf9
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.less
@@ -0,0 +1,41 @@
+/*
+* 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.
+*/
+
+.content-div {
+ min-height: unset;
+
+ .table-header-section {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .table-filter-section {
+ font-size: 14px;
+ font-weight: normal;
+ display: flex;
+ column-gap: 8px;
+ padding: 16px 8px;
+ }
+ }
+
+ .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/volumes/volumes.tsx
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx
new file mode 100644
index 0000000000..a5918ac6ce
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/volumes/volumes.tsx
@@ -0,0 +1,353 @@
+/*
+ * 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, 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 Search from '@/v2/components/search/search';
+
+import { byteToSize, showDataFetchError } from '@/utils/common';
+import { AutoReloadHelper } from '@/utils/autoReloadHelper';
+import { AxiosGetHelper } from "@/utils/axiosRequestHelper";
+import { useDebounce } from '@/v2/hooks/debounce.hook';
+
+import {
+ Volume,
+ VolumesState,
+ VolumesResponse
+} from '@/v2/types/volume.types';
+
+import './volumes.less';
+
+const SearchableColumnOpts = [
+ {
+ label: 'Volume',
+ value: 'volume'
+ },
+ {
+ label: 'Owner',
+ value: 'owner'
+ },
+ {
+ label: 'Admin',
+ value: 'admin'
+ }
+]
+
+const LIMIT_OPTIONS: Option[] = [
+ { label: '1000', value: '1000' },
+ { label: '5000', value: "5000" },
+ { label: '10000', value: "10000" },
+ { label: '20000', value: "20000" }
+]
+
+const Volumes: React.FC<{}> = () => {
+
+ let cancelSignal: 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,
+ }));
+
+ const [state, setState] = useState<VolumesState>({
+ data: [],
+ lastUpdated: 0,
+ columnOptions: defaultColumns,
+ currentRow: {}
+ });
+ const [loading, setLoading] = useState<boolean>(false);
+ const [selectedColumns, setSelectedColumns] =
useState<Option[]>(defaultColumns);
+ const [selectedLimit, setSelectedLimit] = useState<Option>(LIMIT_OPTIONS[0]);
+ const [searchColumn, setSearchColumn] = useState<'volume' | 'owner' |
'admin'>('volume');
+ const [searchTerm, setSearchTerm] = useState<string>('');
+ const [showPanel, setShowPanel] = useState<boolean>(false);
+
+ const debouncedSearch = useDebounce(searchTerm, 300);
+
+ const loadData = () => {
+ setLoading(true);
+
+ const { request, controller } = AxiosGetHelper(
+ '/api/v1/volumes',
+ cancelSignal,
+ "",
+ { limit: selectedLimit.value }
+ );
+
+ cancelSignal = controller;
+ request.then(response => {
+ const volumesResponse: VolumesResponse = response.data;
+ const volumes: Volume[] = volumesResponse.volumes;
+ const data: Volume[] = volumes.map(volume => {
+ return {
+ volume: volume.volume,
+ owner: volume.owner,
+ admin: volume.admin,
+ creationTime: volume.creationTime,
+ modificationTime: volume.modificationTime,
+ quotaInBytes: volume.quotaInBytes,
+ quotaInNamespace: volume.quotaInNamespace,
+ usedNamespace: volume.usedNamespace,
+ acls: volume.acls
+ };
+ });
+
+ setState({
+ ...state,
+ data,
+ lastUpdated: Number(moment()),
+ });
+ setLoading(false);
+ }).catch(error => {
+ setLoading(false);
+ showDataFetchError(error.toString());
+ });
+ };
+
+ let autoReloadHelper: AutoReloadHelper = new AutoReloadHelper(loadData);
+
+ useEffect(() => {
+ loadData();
+ autoReloadHelper.startPolling();
+
+ // Component will unmount
+ return (() => {
+ autoReloadHelper.stopPolling();
+ cancelSignal && cancelSignal.abort();
+ })
+ }, []);
+
+ // If limit changes, load new data
+ useEffect(() => {
+ loadData();
+ }, [selectedLimit.value]);
+
+ function handleColumnChange(selected: ValueType<Option, true>) {
+ setSelectedColumns(selected as Option[]);
+ }
+
+ function handleLimitChange(selected: ValueType<Option, false>) {
+ setSelectedLimit(selected as Option);
+ }
+
+ function handleTagClose(label: string) {
+ setSelectedColumns(
+ selectedColumns.filter((column) => column.label !== label)
+ )
+ }
+
+
+ function handleAclLinkClick(volume: Volume) {
+ setState({
+ ...state,
+ currentRow: 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
+ } = state;
+
+ return (
+ <>
+ <div className='page-header-v2'>
+ Volumes
+ <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={handleTagClose}
+ fixedColumn='Volume'
+ isOptionDisabled={(option) => option.value === 'volume'}
+ columnLength={COLUMNS.length} />
+ <SingleSelect
+ options={LIMIT_OPTIONS}
+ defaultValue={selectedLimit}
+ placeholder='Limit'
+ onChange={handleLimitChange} />
+ </div>
+ <Search
+ searchOptions={SearchableColumnOpts}
+ searchInput={searchTerm}
+ searchColumn={searchColumn}
+ onSearchChange={
+ (e: React.ChangeEvent<HTMLInputElement>) =>
setSearchTerm(e.target.value)
+ }
+ onChange={(value) => {
+ setSearchTerm('');
+ 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>
+ </div>
+ <AclPanel
+ visible={showPanel}
+ acls={currentRow.acls}
+ entityName={currentRow.volume}
+ entityType='Volume'
+ onClose={() => setShowPanel(false)}/>
+ </div>
+ </>
+ );
+}
+
+export default Volumes;
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 4cdd700d50..5d71024616 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
@@ -15,12 +15,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import { lazy } from 'react';
-import Overview from '@/v2/pages/overview/overview';
+const Overview = lazy(() => import('@/v2/pages/overview/overview'));
+const Volumes = lazy(() => import('@/v2/pages/volumes/volumes'))
-export const routesV2: IRoute[] = [
+export const routesV2 = [
{
path: '/Overview',
component: Overview
+ },
+ {
+ path: '/Volumes',
+ component: Volumes
}
];
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/types/acl.types.ts
similarity index 63%
copy from
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/routes-v2.tsx
copy to
hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/acl.types.ts
index 4cdd700d50..33cd047d18 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/types/acl.types.ts
@@ -16,11 +16,32 @@
* limitations under the License.
*/
-import Overview from '@/v2/pages/overview/overview';
+export const ACLIdentityTypeList = [
+ 'USER',
+ 'GROUP',
+ 'WORLD',
+ 'ANONYMOUS',
+ 'CLIENT_IP'
+] as const;
+export type ACLIdentity = typeof ACLIdentityTypeList[number];
-export const routesV2: IRoute[] = [
- {
- path: '/Overview',
- component: Overview
- }
-];
+export const ACLRightList = [
+ 'READ',
+ 'WRITE',
+ 'CREATE',
+ 'LIST',
+ 'DELETE',
+ 'READ_ACL',
+ 'WRITE_ACL',
+ 'ALL',
+ 'NONE'
+] as const;
+export type ACLRight = typeof ACLRightList[number];
+
+
+export type Acl = {
+ type: string;
+ name: string;
+ scope: string;
+ aclList: string[];
+}
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
new file mode 100644
index 0000000000..8b2fd0c694
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/bucket.types.ts
@@ -0,0 +1,55 @@
+/*
+ * 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 { Acl } from "@/v2/types/acl.types";
+
+// Corresponds to OzoneManagerProtocolProtos.StorageTypeProto
+export const BucketStorageTypeList = [
+ 'RAM_DISK',
+ 'SSD',
+ 'DISK',
+ 'ARCHIVE'
+] as const;
+export type BucketStorage = typeof BucketStorageTypeList[number];
+
+// Corresponds to OzoneManagerProtocolProtos.BucketLayoutProto
+export const BucketLayoutTypeList = [
+ 'FILE_SYSTEM_OPTIMIZED',
+ 'OBJECT_STORE',
+ 'LEGACY'
+] as const;
+export type BucketLayout = typeof BucketLayoutTypeList[number];
+
+
+export type Bucket = {
+ volumeName: string;
+ bucketName: string;
+ isVersionEnabled: boolean;
+ storageType: BucketStorage;
+ creationTime: number;
+ modificationTime: number;
+ sourceVolume?: string;
+ sourceBucket?: string;
+ usedBytes: number;
+ usedNamespace: number;
+ quotaInBytes: number;
+ quotaInNamespace: number;
+ owner: string;
+ acls?: Acl[];
+ bucketLayout: BucketLayout;
+}
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
new file mode 100644
index 0000000000..67f007706a
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/types/volume.types.ts
@@ -0,0 +1,44 @@
+/*
+* 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 { Acl } from "@/v2/types/acl.types";
+import { Option } from "@/v2/components/select/multiSelect";
+
+export type Volume = {
+ volume: string;
+ owner: string;
+ admin: string;
+ creationTime: number;
+ modificationTime: number;
+ quotaInBytes: number;
+ quotaInNamespace: number;
+ usedNamespace: number;
+ acls?: Acl[];
+}
+
+export type VolumesResponse = {
+ totalCount: number;
+ volumes: Volume[];
+}
+
+export type VolumesState = {
+ data: Volume[];
+ lastUpdated: number;
+ columnOptions: Option[];
+ currentRow: Volume | Record<string, never>;
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]