From 948db40b104db88affe24f28e9c2ca6824e88269 Mon Sep 17 00:00:00 2001 From: theocerutti Date: Tue, 13 Apr 2021 11:26:56 +0200 Subject: [PATCH] Enhance UX on Bucket & Data Browser: ZENKO-3185, ZENKO-3202, ZENKO-3212, ZENKO-3216, ZENKO-3219 --- assetsTransformer.js | 7 ++ jest.config.js | 2 + package-lock.json | 4 +- package.json | 2 +- public/assets/logo-multi-buckets.svg | 4 ++ src/react/ZenkoUI.js | 11 ++-- src/react/databrowser/buckets/BucketHead.jsx | 31 ++++----- src/react/databrowser/objects/ObjectHead.jsx | 15 +++-- .../databrowser/objects/ObjectListTable.jsx | 30 ++++----- src/react/databrowser/objects/Objects.jsx | 2 +- .../objects/__tests__/ObjectList.test.jsx | 5 +- .../objects/details/Properties.jsx | 10 ++- src/react/ui-elements/Breadcrumb.jsx | 6 +- src/react/ui-elements/Container.jsx | 2 +- src/react/ui-elements/ListLayout2.jsx | 46 +++++++++++-- src/react/ui-elements/ScrollbarWrapper.jsx | 46 +++++++++++++ src/react/ui-elements/TableKeyValue.jsx | 4 ++ src/react/ui-elements/TextBadge.jsx | 9 +++ src/react/ui-elements/TruncatedText.jsx | 66 +++++++++++++++++++ .../__tests__/TruncatedText.test.jsx | 29 ++++++++ 20 files changed, 276 insertions(+), 55 deletions(-) create mode 100644 assetsTransformer.js create mode 100644 public/assets/logo-multi-buckets.svg create mode 100644 src/react/ui-elements/ScrollbarWrapper.jsx create mode 100644 src/react/ui-elements/TextBadge.jsx create mode 100644 src/react/ui-elements/TruncatedText.jsx create mode 100644 src/react/ui-elements/__tests__/TruncatedText.test.jsx diff --git a/assetsTransformer.js b/assetsTransformer.js new file mode 100644 index 000000000..9d8daebc6 --- /dev/null +++ b/assetsTransformer.js @@ -0,0 +1,7 @@ +const path = require('path'); + +module.exports = { + process(src, filename, config, options) { + return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; + }, +}; diff --git a/jest.config.js b/jest.config.js index 918e3bcdf..631b569e7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,6 +2,8 @@ module.exports = { testMatch: [ '**/__tests__/?(*.)+(test).js?(x)'], moduleNameMapper: { '\\.(css|scss)$': 'identity-obj-proxy', + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/assetsTransformer.js', + '\\.(css|less)$': '/assetsTransformer.js', }, setupFiles: ['/.jest-setup.js'], }; diff --git a/package-lock.json b/package-lock.json index 9fe39871a..842bde6b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2118,8 +2118,8 @@ } }, "@scality/core-ui": { - "version": "github:scality/core-ui#5b307a4023bb3fd4f249a6d29fc2256bd3dc73eb", - "from": "github:scality/core-ui#v0.16.0" + "version": "github:scality/core-ui#c2dead8a04305f355b5616185fea890fb7bff18a", + "from": "github:scality/core-ui#v0.18.0" }, "@sindresorhus/is": { "version": "0.7.0", diff --git a/package.json b/package.json index cfe685597..4866944c7 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@fortawesome/fontawesome-free": "5.7.2", "@hapi/joi": "^17.1.1", "@hookform/resolvers": "^0.1.0", - "@scality/core-ui": "github:scality/core-ui.git#v0.16.0", + "@scality/core-ui": "github:scality/core-ui.git#v0.18.0", "async": "^3.2.0", "aws-sdk": "^2.616.0", "connected-react-router": "^6.7.0", diff --git a/public/assets/logo-multi-buckets.svg b/public/assets/logo-multi-buckets.svg new file mode 100644 index 000000000..ee98db9c6 --- /dev/null +++ b/public/assets/logo-multi-buckets.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/react/ZenkoUI.js b/src/react/ZenkoUI.js index b5e766c77..c16ab3a52 100644 --- a/src/react/ZenkoUI.js +++ b/src/react/ZenkoUI.js @@ -9,6 +9,7 @@ import ErrorHandlerModal from './ui-elements/ErrorHandlerModal'; import Loader from './ui-elements/Loader'; import ReauthDialog from './ui-elements/ReauthDialog'; import Routes from './Routes'; +import ScrollbarWrapper from './ui-elements/ScrollbarWrapper'; import { ThemeProvider } from 'styled-components'; import { loadAppConfig } from './actions'; @@ -52,10 +53,12 @@ function ZenkoUI() { } return - - - {content()} - + { /* TODO: replace with core-ui scrollbar when colors are set correctly */ } + + + {content()} + + ; } diff --git a/src/react/databrowser/buckets/BucketHead.jsx b/src/react/databrowser/buckets/BucketHead.jsx index f1cf62941..a3436d962 100644 --- a/src/react/databrowser/buckets/BucketHead.jsx +++ b/src/react/databrowser/buckets/BucketHead.jsx @@ -1,28 +1,29 @@ // @flow import * as L from '../../ui-elements/ListLayout2'; - +import MultiBucketsLogo from '../../../../public/assets/logo-multi-buckets.svg'; import React from 'react'; import type { S3BucketList } from '../../../types/s3'; -import styled from 'styled-components'; - -const Number = styled.div` - font-size: 50px; -`; - -const Unit = styled.div` -`; - +import { TextBadge } from '../../ui-elements/TextBadge'; type Props = { buckets: S3BucketList, }; -export default function BucketHead({ buckets }: Props){ +export default function BucketHead({ buckets }: Props) { + const bucketLength = buckets ? buckets.size : 0; + return - - { buckets.size } - - bucket{ buckets.size > 1 && 's' } + + Multi Buckets Logo + + + + + All Buckets + + + + ; } diff --git a/src/react/databrowser/objects/ObjectHead.jsx b/src/react/databrowser/objects/ObjectHead.jsx index f4fc5721c..0fd62fcb5 100644 --- a/src/react/databrowser/objects/ObjectHead.jsx +++ b/src/react/databrowser/objects/ObjectHead.jsx @@ -1,14 +1,21 @@ -// @flow +// @noflow import * as L from '../../ui-elements/ListLayout2'; import React from 'react'; type Props = { - bucketNameParam?: string, + bucketName?: string, }; -export default function ObjectHead({ bucketNameParam }: Props){ +export default function ObjectHead({ bucketName }: Props) { return - {bucketNameParam} + + + + + { bucketName } + + + ; } diff --git a/src/react/databrowser/objects/ObjectListTable.jsx b/src/react/databrowser/objects/ObjectListTable.jsx index 95b90d93d..c4f137be5 100644 --- a/src/react/databrowser/objects/ObjectListTable.jsx +++ b/src/react/databrowser/objects/ObjectListTable.jsx @@ -7,6 +7,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { useFilters, useFlexLayout, useSortBy, useTable } from 'react-table'; import type { AppState } from '../../../types/state'; import { AutoSizer } from 'react-virtualized'; +import { Checkbox } from '../../ui-elements/FormLayout'; import { FixedSizeList } from 'react-window'; import InfiniteLoader from 'react-window-infinite-loader'; import { List } from 'immutable'; @@ -57,23 +58,22 @@ export default function ObjectListTable({ objects, bucketName, toggled, isVersio accessor: '', Cell({ row: { original } }: CellProps) { return ( - { - e.stopPropagation(); // Prevent checkbox and clickable table row conflict. - dispatch(toggleObject(original.key, original.versionId)); - }} - /> +
+ e.stopPropagation() } // Prevent checkbox and clickable table row conflict. + onChange={() => dispatch(toggleObject(original.key, original.versionId)) } + /> +
); }, - Header: dispatch(toggleAllObjects(!isToggledFull))} - />, + Header: + dispatch(toggleAllObjects(!isToggledFull))} + />, disableSortBy: true, width: 1, }, diff --git a/src/react/databrowser/objects/Objects.jsx b/src/react/databrowser/objects/Objects.jsx index 83e54f499..71045c6aa 100644 --- a/src/react/databrowser/objects/Objects.jsx +++ b/src/react/databrowser/objects/Objects.jsx @@ -74,7 +74,7 @@ export default function Objects(){ - + diff --git a/src/react/databrowser/objects/__tests__/ObjectList.test.jsx b/src/react/databrowser/objects/__tests__/ObjectList.test.jsx index e788159ea..dcb6392a4 100644 --- a/src/react/databrowser/objects/__tests__/ObjectList.test.jsx +++ b/src/react/databrowser/objects/__tests__/ObjectList.test.jsx @@ -1,11 +1,11 @@ import * as s3object from '../../../actions/s3object'; import { BUCKET_INFO, FIRST_FORMATTED_OBJECT, SECOND_FORMATTED_OBJECT } from './utils/testUtil'; import { LIST_OBJECTS_METADATA_TYPE, LIST_OBJECTS_S3_TYPE } from '../../../utils/s3'; +import { checkBox, reduxMount } from '../../../utils/test'; import { BUCKET_NAME } from '../../../actions/__tests__/utils/testUtil'; import { List } from 'immutable'; import ObjectList from '../ObjectList'; import React from 'react'; -import { reduxMount } from '../../../utils/test'; describe('ObjectList', () => { afterEach(() => { @@ -84,8 +84,7 @@ describe('ObjectList', () => { bucketName={BUCKET_NAME} prefixWithSlash='' toggled={List()} bucketInfo={BUCKET_INFO}/>, ); - const toggleAllObjectsButton = component.find('th#object-list-table-head-checkbox > input'); - toggleAllObjectsButton.simulate('change'); + checkBox(component, 'objectsHeaderCheckbox', true); expect(toggleAllObjectsSpy).toHaveBeenCalledTimes(1); }); diff --git a/src/react/databrowser/objects/details/Properties.jsx b/src/react/databrowser/objects/details/Properties.jsx index 1ed805526..c229e23db 100644 --- a/src/react/databrowser/objects/details/Properties.jsx +++ b/src/react/databrowser/objects/details/Properties.jsx @@ -4,11 +4,17 @@ import { formatBytes, formatDate } from '../../../utils'; import { Clipboard } from '../../../ui-elements/Clipboard'; import type { ObjectMetadata } from '../../../../types/s3'; import React from 'react'; +import TruncatedText from '../../../ui-elements/TruncatedText'; +import styled from 'styled-components'; type Props = { objectMetadata: ObjectMetadata, }; +const TruncatedValue = styled(T.Value)` + max-width: 300px; +`; + function Properties({ objectMetadata }: Props) { return (
@@ -20,7 +26,7 @@ function Properties({ objectMetadata }: Props) { @@ -33,7 +39,7 @@ function Properties({ objectMetadata }: Props) { ETag - {objectMetadata.eTag} + {objectMetadata.eTag} diff --git a/src/react/ui-elements/Breadcrumb.jsx b/src/react/ui-elements/Breadcrumb.jsx index 921ca125e..f7b0c9c1c 100644 --- a/src/react/ui-elements/Breadcrumb.jsx +++ b/src/react/ui-elements/Breadcrumb.jsx @@ -36,7 +36,7 @@ const breadcrumbPaths = (pathname: string): Array> => { return ; }); return [ - , + , , ...splitLabels, ]; @@ -44,14 +44,14 @@ const breadcrumbPaths = (pathname: string): Array> => { match = matchPath(pathname, { path: '/buckets/:bucketName/objects' }); if (match) { return [ - , + , , ]; } match = matchPath(pathname, { path: '/buckets/:bucketName' }); if (match) { return [ - , + , ]; } return []; diff --git a/src/react/ui-elements/Container.jsx b/src/react/ui-elements/Container.jsx index 696e8df2c..0ec7149b8 100644 --- a/src/react/ui-elements/Container.jsx +++ b/src/react/ui-elements/Container.jsx @@ -5,7 +5,7 @@ const MainContainer = styled.div` display: flex; width: 100%; height: 100vh; - overflow: scroll; + overflow: auto; color: ${props => props.theme.brand.textPrimary}; font-size: ${fontSize.base}; diff --git a/src/react/ui-elements/ListLayout2.jsx b/src/react/ui-elements/ListLayout2.jsx index 2686cc04b..5ee5cb41f 100644 --- a/src/react/ui-elements/ListLayout2.jsx +++ b/src/react/ui-elements/ListLayout2.jsx @@ -31,20 +31,58 @@ export const ContentContainer = styled.div` export const Head = styled.div` display: flex; - flex-direction: row; - justify-content: space-between; min-height: 80px; padding: ${padding.base}; + padding-left: 32px; background-color: ${props => props.theme.brand.backgroundLevel3}; `; +export const HeadContainer = styled.div` + display: flex; + align-items: center; +`; + +export const HeadTitleContainer = styled.div` + display: flex; + margin-top: ${padding.base}; +`; + +export const HeadTitle = styled.div` + display: flex; + color: ${props => props.theme.brand.textSecondary}; + font-size: ${fontSize.large}; + margin-right: ${padding.small}; + align-items: center; +`; + export const HeadSlice = styled.div` display: flex; - flex: 0 1 100px; - flex-direction: column; justify-content: center; + align-self: center; + text-align: center; + margin-right: ${padding.large}; +`; + +export const HeadBody = styled.div` +`; + +export const HeadIcon = styled.i` + display: flex; + color: ${props => props.theme.brand.statusHealthy}; + background-color: ${props => props.theme.brand.backgroundLevel1}; + border-radius: 100%; + border: 1px solid ${props => props.theme.brand.infoPrimary}; + width: 80px; + height: 80px; + text-align: center; + line-height: 80px; + vertical-align: middle; + margin-right: ${padding.base}; + font-size: 32px; + align-items: center; + justify-content: center; `; export const Body = styled.div` diff --git a/src/react/ui-elements/ScrollbarWrapper.jsx b/src/react/ui-elements/ScrollbarWrapper.jsx new file mode 100644 index 000000000..016c28d71 --- /dev/null +++ b/src/react/ui-elements/ScrollbarWrapper.jsx @@ -0,0 +1,46 @@ +import styled, { css } from 'styled-components'; + +const ScrollbarWrapper = styled.div` + ${props => { + const brand = props.theme.brand; + + return css` + * { + // Chrome / Safari / Edge + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + ::-webkit-scrollbar-track { + background: ${brand.backgroundLevel1}; + } + + ::-webkit-scrollbar-thumb { + width: 4px; + height: 4px; + background: ${brand.buttonSecondary}; + border-radius: 4px; + -webkit-border-radius: 4px; + background-clip: padding-box; + border: 2px solid rgba(0, 0, 0, 0); + } + + ::-webkit-scrollbar-button { + width: 0; + height: 0; + display: none; + } + ::-webkit-scrollbar-corner { + background-color: transparent; + } + + // Firefox + scrollbar-color: ${brand.buttonSecondary} ${brand.backgroundLevel1}; + scrollbar-width: thin; + } + `; + }} +`; + +export default ScrollbarWrapper; diff --git a/src/react/ui-elements/TableKeyValue.jsx b/src/react/ui-elements/TableKeyValue.jsx index 1efef1ace..7c9bc481e 100644 --- a/src/react/ui-elements/TableKeyValue.jsx +++ b/src/react/ui-elements/TableKeyValue.jsx @@ -26,15 +26,19 @@ export const Row = styled.tr` `; export const Key = styled.td` + white-space: nowrap; padding: ${padding.small} 0px; color: ${props => props.theme.brand.textSecondary}; `; export const Value = styled.td` padding-left: 40px; + max-width: 400px; width: 400px; min-width: 220px; word-break: break-word; + + ${props => props.copiable ? `color: ${props.theme.brand.textSecondary};` : ''} `; export const ExtraCell = styled.td` diff --git a/src/react/ui-elements/TextBadge.jsx b/src/react/ui-elements/TextBadge.jsx new file mode 100644 index 000000000..60a7ea611 --- /dev/null +++ b/src/react/ui-elements/TextBadge.jsx @@ -0,0 +1,9 @@ +import { fontSize, padding } from '@scality/core-ui/dist/style/theme'; +import { TextBadge as BasicTextBadge } from '@scality/core-ui'; +import styled from 'styled-components'; + +export const TextBadge = styled(BasicTextBadge)` + padding: ${padding.smaller} ${padding.smaller}; + margin: 0 ${padding.small} 0 ${padding.small}; + font-size: ${fontSize.small}; +`; diff --git a/src/react/ui-elements/TruncatedText.jsx b/src/react/ui-elements/TruncatedText.jsx new file mode 100644 index 000000000..a3a4df851 --- /dev/null +++ b/src/react/ui-elements/TruncatedText.jsx @@ -0,0 +1,66 @@ +// @flow + +import React from 'react'; +import { Tooltip } from '@scality/core-ui'; +import styled from 'styled-components'; + +type Props = { + text: string, + trailingCharCount?: number, +}; + +const TextStart = styled.div` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-shrink: 1; +`; + +const TextEnd = styled.div` + white-space: nowrap; + flex-basis: content; + flex-grow: 0; + flex-shrink: 0; +`; + +const TruncatedContainer = styled.div` + & > div { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + } +`; + +const TruncatedText = ({ + text, + trailingCharCount = 7, +}: Props) => { + + if (!text) + return null; + + const shrinkString = (originStr, trailingCharCount) => { + if (originStr.length > trailingCharCount) { + const front = originStr.substr(0, originStr.length - trailingCharCount); + const end = originStr.substr(-trailingCharCount); + return [front, end]; + } + return [originStr, null]; + }; + + const [front, end] = shrinkString(text, trailingCharCount); + + const content = () => ( + + { front } + { end && { end } } + + ); + + return + { end ? content() : content().props.children } + ; +}; + +export default TruncatedText; diff --git a/src/react/ui-elements/__tests__/TruncatedText.test.jsx b/src/react/ui-elements/__tests__/TruncatedText.test.jsx new file mode 100644 index 000000000..dcb789ee2 --- /dev/null +++ b/src/react/ui-elements/__tests__/TruncatedText.test.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import TruncatedText from '../TruncatedText'; +import { reduxMount } from '../../utils/test'; + +describe('TruncatedText', () => { + it('TruncatedText should render with a truncated-end text', () => { + const { component } = reduxMount(); + + const truncatedEnd = component.find('div.truncated-end'); + expect(truncatedEnd.text()).toContain('ntence.'); + expect(truncatedEnd).toHaveLength(1); + expect(component.find('div.sc-tooltip')).toHaveLength(1); + }); + + it('TruncatedText should render without a truncated-end text', () => { + const { component } = reduxMount(); + + expect(component.find('div.truncated-end')).toHaveLength(0); + expect(component.find('div.sc-tooltip')).toHaveLength(0); + }); + + it('TruncatedText with trailingCharCount length of text', () => { + const text = 'A sentence'; + const { component } = reduxMount(); + + expect(component.find('div.truncated-end')).toHaveLength(0); + expect(component.find('div.sc-tooltip')).toHaveLength(0); + }); +});