[FLASK] Snaps Insight (#15814)
Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com> Co-authored-by: Hassan Malik <41640681+hmalik88@users.noreply.github.com>feature/default_network_editable
parent
8b5630025b
commit
c9dc59ea2a
@ -0,0 +1 @@ |
||||
export { SnapInsight } from './snap-insight'; |
@ -0,0 +1,3 @@ |
||||
.snap-insight { |
||||
word-wrap: break-word; |
||||
} |
@ -0,0 +1,111 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
|
||||
import Preloader from '../../../ui/icon/preloader/preloader-icon.component'; |
||||
import Typography from '../../../ui/typography/typography'; |
||||
import { |
||||
ALIGN_ITEMS, |
||||
COLORS, |
||||
FLEX_DIRECTION, |
||||
JUSTIFY_CONTENT, |
||||
TEXT_ALIGN, |
||||
TYPOGRAPHY, |
||||
} from '../../../../helpers/constants/design-system'; |
||||
import { useI18nContext } from '../../../../hooks/useI18nContext'; |
||||
import { useTransactionInsightSnap } from '../../../../hooks/flask/useTransactionInsightSnap'; |
||||
import SnapContentFooter from '../../flask/snap-content-footer/snap-content-footer'; |
||||
import Box from '../../../ui/box/box'; |
||||
|
||||
export const SnapInsight = ({ transaction, chainId, selectedSnap }) => { |
||||
const t = useI18nContext(); |
||||
const response = useTransactionInsightSnap({ |
||||
transaction, |
||||
chainId, |
||||
snapId: selectedSnap.id, |
||||
}); |
||||
|
||||
const data = response?.insights; |
||||
|
||||
const hasNoData = !data || !Object.keys(data).length; |
||||
|
||||
return ( |
||||
<Box |
||||
flexDirection={FLEX_DIRECTION.COLUMN} |
||||
height="full" |
||||
marginTop={hasNoData && 12} |
||||
marginBottom={hasNoData && 12} |
||||
alignItems={hasNoData && ALIGN_ITEMS.CENTER} |
||||
justifyContent={hasNoData && JUSTIFY_CONTENT.CENTER} |
||||
textAlign={hasNoData && TEXT_ALIGN.CENTER} |
||||
className="snap-insight" |
||||
> |
||||
{data ? ( |
||||
<Box |
||||
height="full" |
||||
flexDirection={FLEX_DIRECTION.COLUMN} |
||||
className="snap-insight__container" |
||||
> |
||||
{Object.keys(data).length ? ( |
||||
<> |
||||
<Box |
||||
flexDirection={FLEX_DIRECTION.COLUMN} |
||||
paddingTop={0} |
||||
paddingRight={6} |
||||
paddingBottom={3} |
||||
paddingLeft={6} |
||||
className="snap-insight__container__data" |
||||
> |
||||
{Object.keys(data).map((key, i) => ( |
||||
<div key={i}> |
||||
<Typography |
||||
fontWeight="bold" |
||||
marginTop={3} |
||||
variant={TYPOGRAPHY.H6} |
||||
> |
||||
{key} |
||||
</Typography> |
||||
<Typography variant={TYPOGRAPHY.H6}>{data[key]}</Typography> |
||||
</div> |
||||
))} |
||||
</Box> |
||||
<SnapContentFooter |
||||
snapName={selectedSnap.manifest.proposedName} |
||||
snapId={selectedSnap.id} |
||||
/> |
||||
</> |
||||
) : ( |
||||
<Typography color={COLORS.TEXT_ALTERNATIVE} variant={TYPOGRAPHY.H6}> |
||||
{t('snapsNoInsight')} |
||||
</Typography> |
||||
)} |
||||
</Box> |
||||
) : ( |
||||
<> |
||||
<Preloader size={40} /> |
||||
<Typography |
||||
marginTop={3} |
||||
color={COLORS.TEXT_ALTERNATIVE} |
||||
variant={TYPOGRAPHY.H6} |
||||
> |
||||
{t('snapsInsightLoading')} |
||||
</Typography> |
||||
</> |
||||
)} |
||||
</Box> |
||||
); |
||||
}; |
||||
|
||||
SnapInsight.propTypes = { |
||||
/* |
||||
* The transaction data object |
||||
*/ |
||||
transaction: PropTypes.object, |
||||
/* |
||||
* CAIP2 Chain ID |
||||
*/ |
||||
chainId: PropTypes.string, |
||||
/* |
||||
* The insight snap selected |
||||
*/ |
||||
selectedSnap: PropTypes.object, |
||||
}; |
@ -0,0 +1 @@ |
||||
export { default } from './snap-content-footer'; |
@ -0,0 +1,13 @@ |
||||
.snap-content-footer { |
||||
i { |
||||
color: var(--color-icon-muted); |
||||
padding-right: 4px; |
||||
} |
||||
|
||||
.button { |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
width: 100px; |
||||
} |
||||
} |
@ -0,0 +1,56 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
|
||||
import { useHistory } from 'react-router-dom'; |
||||
|
||||
import Typography from '../../../ui/typography/typography'; |
||||
import { useI18nContext } from '../../../../hooks/useI18nContext'; |
||||
import { SNAPS_VIEW_ROUTE } from '../../../../helpers/constants/routes'; |
||||
import { |
||||
COLORS, |
||||
TYPOGRAPHY, |
||||
JUSTIFY_CONTENT, |
||||
ALIGN_ITEMS, |
||||
} from '../../../../helpers/constants/design-system'; |
||||
import Button from '../../../ui/button'; |
||||
import Box from '../../../ui/box/box'; |
||||
|
||||
export default function SnapContentFooter({ snapName, snapId }) { |
||||
const t = useI18nContext(); |
||||
const history = useHistory(); |
||||
|
||||
const handleNameClick = (e) => { |
||||
e.stopPropagation(); |
||||
history.push(`${SNAPS_VIEW_ROUTE}/${encodeURIComponent(snapId)}`); |
||||
}; |
||||
// TODO: add truncation to the snap name, need to pick a character length at which to cut off
|
||||
return ( |
||||
<Box |
||||
justifyContent={JUSTIFY_CONTENT.CENTER} |
||||
alignItems={ALIGN_ITEMS.CENTER} |
||||
paddingTop={4} |
||||
paddingBottom={4} |
||||
className="snap-content-footer" |
||||
> |
||||
<i className="fas fa-exclamation-circle fa-sm" /> |
||||
<Typography color={COLORS.TEXT_MUTED} variant={TYPOGRAPHY.H7}> |
||||
{t('snapContent', [ |
||||
<Button type="inline" onClick={handleNameClick} key="button"> |
||||
{snapName} |
||||
</Button>, |
||||
])} |
||||
</Typography> |
||||
</Box> |
||||
); |
||||
} |
||||
|
||||
SnapContentFooter.propTypes = { |
||||
/** |
||||
* The name of the snap who's content is displayed |
||||
*/ |
||||
snapName: PropTypes.string, |
||||
/** |
||||
* The id of the snap |
||||
*/ |
||||
snapId: PropTypes.string, |
||||
}; |
@ -0,0 +1,17 @@ |
||||
import React from 'react'; |
||||
|
||||
import SnapContentFooter from '.'; |
||||
|
||||
export default { |
||||
title: 'Components/App/Flask/SnapContentFooter', |
||||
id: __filename, |
||||
component: SnapContentFooter, |
||||
args: { |
||||
snapName: 'Test Snap', |
||||
snapId: 'local:test-snap', |
||||
}, |
||||
}; |
||||
|
||||
export const DefaultStory = (args) => <SnapContentFooter {...args} />; |
||||
|
||||
DefaultStory.storyName = 'Default'; |
@ -0,0 +1,63 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
import classnames from 'classnames'; |
||||
import Dropdown from '../../dropdown'; |
||||
|
||||
export const DropdownTab = (props) => { |
||||
const { |
||||
activeClassName, |
||||
className, |
||||
'data-testid': dataTestId, |
||||
isActive, |
||||
onClick, |
||||
onChange, |
||||
tabIndex, |
||||
options, |
||||
selectedOption, |
||||
} = props; |
||||
|
||||
return ( |
||||
<li |
||||
className={classnames('tab', className, { |
||||
'tab--active': isActive, |
||||
[activeClassName]: activeClassName && isActive, |
||||
})} |
||||
data-testid={dataTestId} |
||||
onClick={(event) => { |
||||
event.preventDefault(); |
||||
onClick(tabIndex); |
||||
}} |
||||
> |
||||
<Dropdown |
||||
options={options} |
||||
selectedOption={selectedOption} |
||||
onChange={onChange} |
||||
/> |
||||
</li> |
||||
); |
||||
}; |
||||
|
||||
DropdownTab.propTypes = { |
||||
activeClassName: PropTypes.string, |
||||
className: PropTypes.string, |
||||
'data-testid': PropTypes.string, |
||||
isActive: PropTypes.bool, // required, but added using React.cloneElement
|
||||
options: PropTypes.arrayOf( |
||||
PropTypes.exact({ |
||||
name: PropTypes.string, |
||||
value: PropTypes.string.isRequired, |
||||
}), |
||||
).isRequired, |
||||
selectedOption: PropTypes.string, |
||||
onChange: PropTypes.func, |
||||
onClick: PropTypes.func, |
||||
tabIndex: PropTypes.number, // required, but added using React.cloneElement
|
||||
}; |
||||
|
||||
DropdownTab.defaultProps = { |
||||
activeClassName: undefined, |
||||
className: undefined, |
||||
onChange: undefined, |
||||
onClick: undefined, |
||||
selectedOption: undefined, |
||||
}; |
@ -0,0 +1,3 @@ |
||||
import { DropdownTab } from './dropdown-tab'; |
||||
|
||||
export default DropdownTab; |
@ -0,0 +1,23 @@ |
||||
.tab { |
||||
.dropdown__select { |
||||
border: none; |
||||
font-size: unset; |
||||
width: 100%; |
||||
background-color: unset; |
||||
padding-left: 8px; |
||||
padding-right: 20px; |
||||
line-height: unset; |
||||
|
||||
option { |
||||
background-color: var(--color-background-default); |
||||
} |
||||
|
||||
&:focus-visible { |
||||
outline: none; |
||||
} |
||||
} |
||||
|
||||
.dropdown__icon-caret-down { |
||||
right: 0; |
||||
} |
||||
} |
@ -1,4 +1,5 @@ |
||||
import Tabs from './tabs.component'; |
||||
import Tab from './tab'; |
||||
import DropdownTab from './dropdown-tab'; |
||||
|
||||
export { Tabs, Tab }; |
||||
export { Tabs, Tab, DropdownTab }; |
||||
|
@ -0,0 +1,37 @@ |
||||
import { useEffect, useState } from 'react'; |
||||
import { useSelector } from 'react-redux'; |
||||
import { handleSnapRequest } from '../../store/actions'; |
||||
import { getPermissionSubjects } from '../../selectors'; |
||||
|
||||
const INSIGHT_PERMISSION = 'endowment:transaction-insight'; |
||||
|
||||
export function useTransactionInsightSnap({ transaction, chainId, snapId }) { |
||||
const subjects = useSelector(getPermissionSubjects); |
||||
if (!subjects[snapId]?.permissions[INSIGHT_PERMISSION]) { |
||||
throw new Error( |
||||
'This snap does not have the transaction insight endowment.', |
||||
); |
||||
} |
||||
const [data, setData] = useState(undefined); |
||||
|
||||
useEffect(() => { |
||||
async function fetchInsight() { |
||||
const d = await handleSnapRequest({ |
||||
snapId, |
||||
origin: 'test', |
||||
handler: 'onTransaction', |
||||
request: { |
||||
jsonrpc: '2.0', |
||||
method: ' ', |
||||
params: { transaction, chainId }, |
||||
}, |
||||
}); |
||||
setData(d); |
||||
} |
||||
if (transaction) { |
||||
fetchInsight(); |
||||
} |
||||
}, [snapId, transaction, chainId]); |
||||
|
||||
return data; |
||||
} |
Loading…
Reference in new issue