diff --git a/package.json b/package.json index 9a4b582..2ac1fd7 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "typescript": "~4.0.5" }, "devDependencies": { - "@rsksmart/ipfs-cpinner-client": "^0.1.1-beta.2", + "@rsksmart/ipfs-cpinner-client": "^0.1.1-beta.3", "@rsksmart/rlogin": "0.0.1-beta.3", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", diff --git a/src/app/Authenticated/AuthenticatedComponent.tsx b/src/app/Authenticated/AuthenticatedComponent.tsx index d7d31d6..8dcc74a 100644 --- a/src/app/Authenticated/AuthenticatedComponent.tsx +++ b/src/app/Authenticated/AuthenticatedComponent.tsx @@ -14,15 +14,17 @@ const AuthenticatedComponent: React.FC = ({ cha const [screen, setScreen] = useState(screens.DASHBOARD) const context = useContext(Web3ProviderContext) + const changeScreen = (screen: screens) => setScreen(screen) + return ( <> setScreen(screen)} + handleClick={changeScreen} showDataVault={!!context.dvClient} /> - {screen === screens.DASHBOARD && } + {screen === screens.DASHBOARD && } {screen === screens.DATAVAULT && } ) diff --git a/src/app/Dashboard/DashboardContainer.ts b/src/app/Dashboard/DashboardContainer.ts index 310de88..32bc6af 100644 --- a/src/app/Dashboard/DashboardContainer.ts +++ b/src/app/Dashboard/DashboardContainer.ts @@ -17,7 +17,8 @@ const mapStateToProps = (state: stateInterface) => ({ chainId: state.identity.chainId, tokens: state.tokens.tokens, owner: getOwnerFromDidDoc(state.ethrdid.didDocument), - delegates: state.ethrdid.didDocument.authentication?.filter((pk: Authentication) => !pk.publicKey.endsWith('controller')) + delegates: state.ethrdid.didDocument.authentication?.filter((pk: Authentication) => !pk.publicKey.endsWith('controller')), + storage: state.datavault.storage }) const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ diff --git a/src/app/Dashboard/DashboardScreen.tsx b/src/app/Dashboard/DashboardScreen.tsx index 8cc0a3a..b8edb1f 100644 --- a/src/app/Dashboard/DashboardScreen.tsx +++ b/src/app/Dashboard/DashboardScreen.tsx @@ -3,6 +3,9 @@ import IdentityInformationComponent from './panels/IdentityInformation' import { Authentication } from 'did-resolver' import Balance from './panels/Balance' import { Token } from '../state/reducers/tokens' +import DataVaultSummary from './panels/DataVaultSummary' +import { screens } from '../Authenticated/components/Navigation' +import { DataVaultStorageState } from '../state/reducers/datavault' interface DashboardScreenInterface { chainId?: number | null @@ -10,13 +13,15 @@ interface DashboardScreenInterface { owner?: string | null delegates?: Authentication[] tokens?: Token[] + storage?: DataVaultStorageState changeOwner: (provider: any, newOwner: string) => any addDelegate: (provider: any, delegateAddr: string) => any addCustomToken: (provider: any, tokenAddr: string) => any + changeScreen: (screen: string) => void } const DashboardScreen: React.FC = ({ - chainId, address, owner, delegates, tokens, changeOwner, addDelegate, addCustomToken + chainId, address, owner, delegates, tokens, changeOwner, addDelegate, addCustomToken, changeScreen, storage }) => { return (
@@ -32,7 +37,9 @@ const DashboardScreen: React.FC = ({
-
 
+
+ changeScreen(screens.DATAVAULT)} /> +
) diff --git a/src/app/Dashboard/panels/DataVaultSummary.test.tsx b/src/app/Dashboard/panels/DataVaultSummary.test.tsx new file mode 100644 index 0000000..dfd51cd --- /dev/null +++ b/src/app/Dashboard/panels/DataVaultSummary.test.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { mount } from 'enzyme' +import DataVaultSummary from './DataVaultSummary' + +describe('Component: DataVaultSummary.test', () => { + const defaultProps = { storage: { used: 0, available: 0 }, handleButton: jest.fn() } + + it('renders the component', () => { + const wrapper = mount() + expect(wrapper).toBeDefined() + }) + + it('handles DataVault click', () => { + const handleClick = jest.fn() + const wrapper = mount() + + wrapper.find('button').simulate('click') + expect(handleClick).toBeCalledTimes(1) + }) + + it('displays numbers in tooltip', () => { + const props = { storage: { used: 1000, available: 9000 }, handleButton: jest.fn() } + const wrapper = mount() + expect(wrapper.find('.hover-content').first().text()).toBe('1000 of 10,000 bytes') + }) +}) diff --git a/src/app/Dashboard/panels/DataVaultSummary.tsx b/src/app/Dashboard/panels/DataVaultSummary.tsx new file mode 100644 index 0000000..f502912 --- /dev/null +++ b/src/app/Dashboard/panels/DataVaultSummary.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import Panel from '../../../components/Panel/Panel' +import datavaultIcon from '../../../assets/images/icons/data-vault.svg' +import { BaseButton } from '../../../components/Buttons' +import ProgressBar from '../../../components/ProgressBar/ProgressBar' +import ToolTip from '../../../components/Tooltip/Tooltip' +import { DataVaultStorageState } from '../../state/reducers/datavault' + +interface DataVaultSummaryInterface { + storage?: DataVaultStorageState + handleButton: () => void +} + +const DataVaultSummary: React.FC = ({ storage, handleButton }) => + storage ? ( + DataVault DataVault Summary} className="dataVault"> +

Storage Usage

+
+
+ {storage.used} of {(storage.available + storage.used).toLocaleString()} bytes

}> + +
+
+
+ DataVault +
+
+
+ ) + : <> + +export default DataVaultSummary diff --git a/src/app/state/operations/datavault.ts b/src/app/state/operations/datavault.ts index e7ca3f9..5e73ead 100644 --- a/src/app/state/operations/datavault.ts +++ b/src/app/state/operations/datavault.ts @@ -1,7 +1,7 @@ import { Dispatch } from 'react' import DataVaultWebClient from '@rsksmart/ipfs-cpinner-client' import { createDidFormat } from '../../../formatters' -import { addContentToKey, DataVaultContent, receiveKeyData, removeContentfromKey, swapContentById } from '../reducers/datavault' +import { addContentToKey, DataVaultContent, receiveKeyData, removeContentfromKey, swapContentById, receiveStorageInformation, DataVaultStorageState } from '../reducers/datavault' import { getDataVault } from '../../../config/getConfig' import { CreateContentResponse } from '@rsksmart/ipfs-cpinner-client/lib/types' @@ -60,6 +60,21 @@ export const deleteDataVaultContent = (client: DataVaultWebClient, key: string, client.delete({ key, id }) .then(() => dispatch(removeContentfromKey({ key, id }))) +/** + * Swap content in the datavault by key, and Id + * @param client DataVault client + * @param key Key of object + * @param content New content + * @param id id of the content + */ export const swapDataVaultContent = (client: DataVaultWebClient, key: string, content: string, id: string) => (dispatch: Dispatch) => client.swap({ key, content, id }) .then(() => dispatch(swapContentById({ key, id, content }))) + +/** + * Returns storage information from DataVault + * @param client DataVault client + */ +export const getStorageInformation = (client: DataVaultWebClient) => (dispatch: Dispatch) => + client.getStorageInformation() + .then((storage: DataVaultStorageState) => dispatch(receiveStorageInformation({ storage }))) diff --git a/src/app/state/operations/identity.ts b/src/app/state/operations/identity.ts index 26078a0..a13d333 100644 --- a/src/app/state/operations/identity.ts +++ b/src/app/state/operations/identity.ts @@ -5,7 +5,7 @@ import { rLogin } from '../../../features/rLogin' import { changeAccount, changeChainId } from '../reducers/identity' import { resolveDidDocument } from './ethrdid' import { getTokenList } from './tokens' -import { createClient, getDataVaultContent } from './datavault' +import { createClient, getDataVaultContent, getStorageInformation } from './datavault' import { createDidFormat } from '../../../formatters' /** @@ -28,6 +28,7 @@ export const login = (context: any) => (dispatch: Dispatch) => context.setDvClient(dataVaultClient) dataVaultClient && dispatch(getDataVaultContent(dataVaultClient, createDidFormat(address, chainId, true))) + dataVaultClient && dispatch(getStorageInformation(dataVaultClient)) }) }) .catch((err: string) => console.log('rLogin Error', err)) diff --git a/src/app/state/reducers/datavault.test.ts b/src/app/state/reducers/datavault.test.ts index e98289e..85b91fb 100644 --- a/src/app/state/reducers/datavault.test.ts +++ b/src/app/state/reducers/datavault.test.ts @@ -1,5 +1,5 @@ import { configureStore, Store, AnyAction } from '@reduxjs/toolkit' -import dataVaultSlice, { DataVaultState, receiveKeyData, initialState, addContentToKey, removeContentfromKey, swapContentById, DataVaultContent } from './datavault' +import dataVaultSlice, { DataVaultState, receiveKeyData, initialState, addContentToKey, removeContentfromKey, swapContentById, DataVaultContent, receiveStorageInformation } from './datavault' describe('dataVault slice', () => { describe('action creators', () => { @@ -22,6 +22,11 @@ describe('dataVault slice', () => { const content = { key: 'KEY', id: '2', content: 'new' } expect(swapContentById(content)).toEqual({ type: swapContentById.type, payload: content }) }) + + test('receiveStorageInformation', () => { + const storage = { used: 150, available: 200 } + expect(receiveStorageInformation({ storage })).toEqual({ type: receiveStorageInformation.type, payload: { storage } }) + }) }) describe('reducer', () => { @@ -110,5 +115,12 @@ describe('dataVault slice', () => { expect(store.getState().data.MY_KEY).toMatchObject(initContent) }) }) + + describe('receiveStorageInformation', () => { + test('it receives storage information', () => { + store.dispatch(receiveStorageInformation({ storage: { used: 10, available: 15 } })) + expect(store.getState().storage).toMatchObject({ used: 10, available: 15 }) + }) + }) }) }) diff --git a/src/app/state/reducers/datavault.ts b/src/app/state/reducers/datavault.ts index 2679e10..2cc2287 100644 --- a/src/app/state/reducers/datavault.ts +++ b/src/app/state/reducers/datavault.ts @@ -20,12 +20,19 @@ interface SwapPayLoad { content: string } +export interface DataVaultStorageState { + used: number, + available: number, +} + export interface DataVaultState { data: DataVaultKey + storage?: DataVaultStorageState } export const initialState: DataVaultState = { - data: {} + data: {}, + storage: undefined } const dataVaultSlice = createSlice({ @@ -43,10 +50,13 @@ const dataVaultSlice = createSlice({ }, swapContentById (state: DataVaultState, { payload: { key, id, content } }: PayloadAction) { state.data[key] = state.data[key].map((item: DataVaultContent) => item.id === id ? { ...item, content } : item) + }, + receiveStorageInformation (state: DataVaultState, { payload: { storage } }: PayloadAction<{ storage: DataVaultStorageState }>) { + state.storage = storage } } }) -export const { receiveKeyData, addContentToKey, removeContentfromKey, swapContentById } = dataVaultSlice.actions +export const { receiveKeyData, addContentToKey, removeContentfromKey, swapContentById, receiveStorageInformation } = dataVaultSlice.actions export default dataVaultSlice.reducer diff --git a/src/assets/images/icons/data-vault.svg b/src/assets/images/icons/data-vault.svg new file mode 100644 index 0000000..5cd556c --- /dev/null +++ b/src/assets/images/icons/data-vault.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/scss/screens/_dashboard.scss b/src/assets/scss/screens/_dashboard.scss index 0be56fb..48f396f 100644 --- a/src/assets/scss/screens/_dashboard.scss +++ b/src/assets/scss/screens/_dashboard.scss @@ -1,4 +1,11 @@ .dashboard { + h2 { + font-size: 14px; + font-weight: 500 !important; + color: $gray; + text-transform: uppercase; + } + .identity-information { .advancedToggle { border: none; @@ -8,13 +15,6 @@ font-size: 14px; } - h2 { - font-size: 14px; - font-weight: 500 !important; - color: $gray; - text-transform: uppercase; - } - p.value { color: $black; font-size: 18px; @@ -64,4 +64,36 @@ } } } + + .dataVault { + .columnDouble { + padding: 0; + + .tooltip-progress { + display: block; + width: 100%; + } + } + + .column { + padding: 0; + text-align: right; + + button { + padding: 10px 25px; + } + } + } +} + +@media screen and (max-width: $breakpoint-phone) { + .dashboard { + button.panel-cta { + display: block; + position: relative; + bottom: auto; + right: auto; + margin-top: 15px; + } +} } diff --git a/src/components/ProgressBar/ProgressBar.test.tsx b/src/components/ProgressBar/ProgressBar.test.tsx new file mode 100644 index 0000000..92a0ee7 --- /dev/null +++ b/src/components/ProgressBar/ProgressBar.test.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { mount } from 'enzyme' +import ProgressBar from './ProgressBar' + +describe('Component: ProgressBar.test', () => { + it('renders the component', () => { + const wrapper = mount() + expect(wrapper).toBeDefined() + }) + + describe('returns correct progress bar width', () => { + it('5 of 100', () => { + const wrapper = mount() + expect(wrapper.find('.progress')).toBeDefined() + expect(wrapper.find('.progress').first().props().style).toMatchObject({ width: '5%' }) + }) + + it('26 of 150', () => { + const wrapper = mount() + expect(wrapper.find('.progress').first().props().style).toMatchObject({ width: '18%' }) + }) + }) + + it('returns 100% if thge total is higher than the value', () => { + const wrapper = mount() + expect(wrapper.find('.progress').first().props().style).toMatchObject({ width: '100%' }) + }) + + it('returns 0% if the value is 0', () => { + const wrapper = mount() + expect(wrapper.find('.progress').first().props().style).toMatchObject({ width: '0%' }) + }) + + it('shows a visual bar of 1% if the value is less than 1% but not 0', () => { + const wrapper = mount() + expect(wrapper.find('.progress').first().props().style).toMatchObject({ width: '1%' }) + }) +}) diff --git a/src/components/ProgressBar/ProgressBar.tsx b/src/components/ProgressBar/ProgressBar.tsx new file mode 100644 index 0000000..f56686b --- /dev/null +++ b/src/components/ProgressBar/ProgressBar.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import styled from 'styled-components' + +interface ProgressBarInterface { + total: number, + value: number +} + +const BarWrapper = styled.div` + height: 20px; + position: relative; + background: #EAEAEA; + -moz-border-radius: 25px; + -webkit-border-radius: 25px; + border-radius: 12.5px; + padding: 10px; + box-shadow: inset 0 -1px 1px rgba(255,255,255,0.3); +` + +const BarProgress = styled.div` + display: block; + height: 100%; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + background-color: #47C4E1; + box-shadow: + inset 0 2px 9px rgba(255,255,255,0.3), + inset 0 -2px 6px rgba(0,0,0,0.4); + position: relative; + overflow: hidden; + } +` + +const ProgressBar: React.FC = ({ total, value }) => { + const width = value < total ? Math.ceil((value * 100) / total) : 100 + return ( + + + + ) +} + +export default ProgressBar diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index c94917a..ca298ef 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components' interface TooltipInterface { children: ReactNode hoverContent: string | ReactNode + className?: string } const HoverSpan = styled.span` @@ -32,9 +33,9 @@ const HoverTrigger = styled.span` } ` -const ToolTip: React.FC = ({ hoverContent, children }) => ( +const ToolTip: React.FC = ({ hoverContent, children, className }) => ( <> - {children} + {children} {hoverContent} ) diff --git a/src/config/config.testnet.json b/src/config/config.testnet.json index 2c4e891..0cad4bd 100644 --- a/src/config/config.testnet.json +++ b/src/config/config.testnet.json @@ -4,6 +4,6 @@ "tokens": [ "0x19f64674d8a5b4e652319f5e239efd3bc969a1fe" ], "dataVault": { "serviceDid": "did:ethr:rsk:testnet:0x285B30492a3F444d78f75261A35cB292Fc8F41A6", - "serviceUrl": "http://ec2-3-131-142-122.us-east-2.compute.amazonaws.com:5107" + "serviceUrl": "https://identity.staging.rifcomputing.net" } } diff --git a/yarn.lock b/yarn.lock index 52135d3..829fbd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1589,10 +1589,10 @@ ethjs-query "^0.3.8" ethr-did-resolver "^1.0.2" -"@rsksmart/ipfs-cpinner-client@^0.1.1-beta.2": - version "0.1.1-beta.2" - resolved "https://registry.yarnpkg.com/@rsksmart/ipfs-cpinner-client/-/ipfs-cpinner-client-0.1.1-beta.2.tgz#666d2e93e9d8f8585d116a380462120534348e36" - integrity sha512-H9Yp+xJsRnxGd/6JSh6bLXW3ijixtOTwmZjngYGNxABOZ0AEYkRfo7rMrgFQuVM1oNQeuWetrhO2daWiVVYMpA== +"@rsksmart/ipfs-cpinner-client@^0.1.1-beta.3": + version "0.1.1-beta.3" + resolved "https://registry.yarnpkg.com/@rsksmart/ipfs-cpinner-client/-/ipfs-cpinner-client-0.1.1-beta.3.tgz#6cb862a60ffdceb185bd24bab1f3425d9dd1cf83" + integrity sha512-Uh59NfVxje2uwATYjRdw9dvRjqXdhGh3GicJt4VRwbAYLVGJcN4ddFwEyxixjqF9IV++Edpy0PYxdCdAgdRWyA== dependencies: axios "^0.21.0" did-jwt "^4.6.2"