Skip to content
This repository has been archived by the owner on Aug 19, 2020. It is now read-only.

Update org viewer #24

Merged
merged 18 commits into from May 27, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/organization-viewer-web/package.json
Expand Up @@ -4,14 +4,20 @@
"version": "1.0.0",
"private": true,
"scripts": {
"build": "npm run clean && npm run compile",
"clean": "rm -rf ./dist",
"dev": "yarn clean && webpack-dev-server",
"build": "yarn clean && webpack",
"clean": "rm -rf ./dist && rm -f tsconfig.tsbuildinfo",
"compile": "tsc -p tsconfig.json"
},
"dependencies": {
"plumbery-react": "*"
"@emotion/core": "^10.0.28",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀👀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha yes, I wanted to test its JSX pragma for css: https://emotion.sh/docs/css-prop#jsx-pragma

Other than that it’s basically styled-components 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha yeah, I've used emotion before, this option looks cool for inline styling as well! 😃

"@types/react": "^16.9.35",
"plumbery-core": "*",
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"devDependencies": {
"@types/react-dom": "^16.9.8",
"html-webpack-plugin": "^4.3.0",
"ts-loader": "^7.0.2",
"typescript": "^3.8.3",
Expand Down
127 changes: 127 additions & 0 deletions packages/organization-viewer-web/src/App.tsx
@@ -0,0 +1,127 @@
/** @jsx jsx */
import React, { useEffect, useState } from 'react'
import { css, jsx } from '@emotion/core'
import {
connect,
App as AppType,
Organization,
Permission,
} from 'plumbery-core'
import Main from './Main'
import OrgApps from './OrgApps'
import OrgInfo from './OrgInfo'
import OrgPermissions from './OrgPermissions'
import TextButton from './TextButton'

const ORG_ADDRESSES = new Map([
['xyz', '0x0146414e5a819240963450332f647dfb7c722af4'],
['td.aragonid.eth', '0xa9Aad8e278eECf369c42F78D5A3f2d866DE902C8'],
['hive.aragonid.eth', '0xe520428C232F6Da6f694b121181f907931fD2211'],
['mesh.aragonid.eth', '0xa48300a4E89b59A79452Db7d3CD408Df57f4aa78'],
])

function addressFromOrgName(orgName: string) {
return (
ORG_ADDRESSES.get(orgName) ||
ORG_ADDRESSES.get(`${orgName}.aragonid.eth`) ||
orgName
)
}

export default function App() {
const [orgNameValue, setOrgNameValue] = useState('')
const [org, setOrg] = useState<Organization>()
const [path, setPath] = useState('')

const updateOrg = (orgName: string) => {
window.location.hash = `/${orgName}`
}

const openApp = (appAddress: string) => {
window.location.hash = `/${orgNameValue}/${appAddress}`
}

useEffect(() => {
const onChange = () => {
const org = window.location.hash.match(/^#\/([^\/]+)/)?.[1]
setOrgNameValue(org || '')
}

onChange()
window.addEventListener('hashchange', onChange)
updateOrg('xyz')

return () => {
window.removeEventListener('hashchange', onChange)
}
}, [])

useEffect(() => {
let cancelled = false

const fetchOrg = async (orgName: string) => {
const org = await connect(addressFromOrgName(orgName))
if (!cancelled) {
setOrg(org)
}
}

fetchOrg(orgNameValue.trim())

return () => {
cancelled = true
}
}, [orgNameValue])

return (
<Main>
<label>
<div
css={css`
padding-left: 4px;
padding-bottom: 8px;
font-size: 20px;
`}
>
Enter an org location:
</div>
<input
onChange={event => updateOrg(event.target.value)}
placeholder="e.g. xyz.aragonid.eth"
type="text"
value={orgNameValue}
css={css`
width: 100%;
padding: 12px;
border: 2px solid #fad4fa;
border-radius: 6px;
font-size: 24px;
outline: 0;
`}
/>
</label>
<div
css={css`
white-space: nowrap;
padding-top: 8px;
padding-left: 4px;
font-size: 20px;
`}
>
Or pick one:&nbsp;
{[...ORG_ADDRESSES.keys()].map((name, index, items) => (
<span key={name}>
{index > 0 && <span>, </span>}
<TextButton onClick={() => updateOrg(name)}>
{name.match(/^[^\.]+/)?.[0]}
</TextButton>
</span>
))}
.
</div>
<OrgInfo org={org} orgAddress={addressFromOrgName(orgNameValue)} />
<OrgApps org={org} onOpenApp={openApp} />
<OrgPermissions org={org} />
</Main>
)
}
54 changes: 54 additions & 0 deletions packages/organization-viewer-web/src/Group.tsx
@@ -0,0 +1,54 @@
/** @jsx jsx */
import { css, jsx, keyframes } from '@emotion/core'

type GroupProps = {
children: React.ReactNode
loading?: boolean
name: string
}

// const blink = keyframes`50% { opacity: 0 }`
const blink = keyframes`50% { opacity: 1 }`

export default function Group({ children, loading, name }: GroupProps) {
return (
<div
css={css`
padding-top: 64px;
`}
>
<div
css={css`
padding-bottom: 16px;
font-size: 20px;
`}
>
{name}:
{loading && (
<span
css={css`
animation-name: ${blink};
animation-duration: 80ms;
animation-timing-function: steps(1);
animation-iteration-count: infinite;
font-size: 16px;
`}
>
&nbsp;loading…
</span>
)}
</div>
{!loading && (
<div
css={css`
background: #fff;
border: 2px solid #fad4fa;
border-radius: 6px;
`}
>
{children}
</div>
)}
</div>
)
}
56 changes: 56 additions & 0 deletions packages/organization-viewer-web/src/Main.tsx
@@ -0,0 +1,56 @@
/** @jsx jsx */
import { Global, css, jsx } from '@emotion/core'

type MainProps = { children: React.ReactNode }

export default function Main({ children }: MainProps) {
return (
<div>
<div
css={css`
width: 600px;
margin: 0 auto;
padding: 60px 0;
`}
>
<h1
css={css`
font-size: 40px;
margin: 0;
padding: 0 0 60px;
`}
>
Org Viewer
</h1>
<div>{children}</div>
</div>
<Global
styles={css`
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400&display=swap');
*,
*:before,
*:after {
box-sizing: border-box;
}
body {
margin: 0;
background: #faf1fa;
}
body,
h1,
input,
button {
font: 300 24px/1.5 'Fira Code';
color: #222;
}
input[type='text'] {
background: #fff;
&::placeholder {
color: #222;
}
}
`}
/>
</div>
)
}
58 changes: 58 additions & 0 deletions packages/organization-viewer-web/src/OrgApps.tsx
@@ -0,0 +1,58 @@
/** @jsx jsx */
import React, { useEffect, useState } from 'react'
import { css, jsx } from '@emotion/core'
import { App, Organization } from 'plumbery-core'
import Group from './Group'
import Table from './Table'
import TextButton from './TextButton'

type OrgAppsProps = {
org?: Organization
onOpenApp: (address: string) => void
}

export default function OrgApps({ onOpenApp, org }: OrgAppsProps) {
const [apps, setOrgApps] = useState<App[]>([])
const [loading, setLoading] = useState<boolean>(true)

useEffect(() => {
let cancelled = false

const update = async () => {
if (!org) {
return
}

setLoading(true)
const apps = await org.apps()

if (!cancelled) {
setLoading(false)
setOrgApps(apps)
}
}

update()

return () => {
cancelled = true
}
}, [org])

return (
<Group name="Apps" loading={loading}>
<Table
headers={['Name', 'Version', 'Address']}
rows={[...apps]
.sort((a, b) => (a.name || '').localeCompare(b.name || ''))
.map((app: App) => [
app.name || 'unknown',
app.version || '?',
<TextButton onClick={() => onOpenApp(app.address)}>
{app.address.slice(0, 6)}
</TextButton>,
])}
/>
</Group>
)
}
34 changes: 34 additions & 0 deletions packages/organization-viewer-web/src/OrgInfo.tsx
@@ -0,0 +1,34 @@
/** @jsx jsx */
import { css, jsx } from '@emotion/core'
import React from 'react'
import { Organization } from 'plumbery-core'
import Group from './Group'
import Table from './Table'
import TextButton from './TextButton'

type OrgInfoProps = {
org?: Organization
orgAddress: string
}

export default function OrgApps({ org, orgAddress }: OrgInfoProps) {
if (!org) {
return null
}

return (
<Group name="Organization">
<Table
headers={['Name', 'Value']}
rows={Object.entries({ ...org, address: orgAddress }).map(
([name, value]) => {
if (value.startsWith('0x')) {
value = value.slice(0, 6)
}
return [name, value]
}
)}
/>
</Group>
)
}