Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
}
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint"
"@typescript-eslint",
"react"
],
"rules": {
"no-unused-vars": ["warn", {
Expand Down
40 changes: 40 additions & 0 deletions dashboard/dashboardPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react'
import ReactDOM from 'react-dom'
import UI from 'solid-ui'
import panes from 'pane-registry'
import { Wrapper } from './wrapper'
import { PaneDefinition } from '../types'

const HomePane: PaneDefinition = {
icon: UI.icons.iconBase + 'noun_547570.svg', // noun_25830

name: 'dashboard',

label: function () {
return 'Dashboard'
},

render: function (subject, dom) {
const container = document.createElement('div')
const loadResource = (resourcePath: string) => {
panes.getOutliner(dom).GotoSubject(resourcePath, true, undefined, true)
}
UI.authn.solidAuthClient.currentSession().then((session: any) => {
ReactDOM.render(
<Wrapper
store={UI.store}
fetcher={UI.store.fetcher}
updater={UI.store.updater}
webId={session.webId}
loadResource={loadResource}
/>,
container
)
})

return container
}
} // pane object

// ends
export default HomePane
28 changes: 28 additions & 0 deletions dashboard/datasister-dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'
import { ProfileWidget } from './widgets/Profile'
import { BookmarksWidget } from './widgets/Bookmarks'
import { FolderWidget } from './widgets/Folder'
import { AppsWidget } from './widgets/Apps'

export const Dashboard: React.FC<{
}> = (props) => {
return (
<>
<section className="section">
<div className="columns">
<div className="column">
<ProfileWidget/>
</div>
<div className="column">
<AppsWidget/>
</div>
<div className="column">
<FolderWidget/>
</div>
<div className="column">
<BookmarksWidget/>
</div>
</div>
</section>
</>)
}
47 changes: 47 additions & 0 deletions dashboard/datasister-dashboard/components/ResourceLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react'
import { NamedNode } from 'rdflib'
import UI from 'solid-ui'
import { DataBrowserContext } from '../context'

interface OwnProps {
resource: NamedNode;
};

type Props = Omit<React.HTMLAttributes<HTMLAnchorElement>, keyof OwnProps> & OwnProps;

export const ResourceLink: React.FC<Props> = (props) => {
const { store, loadResource, podOrigin } = React.useContext(DataBrowserContext)
const clickHandler = (event: React.MouseEvent) => {
if (props.resource.uri.substring(0, podOrigin.length) === podOrigin) {
event.preventDefault()
loadResource(props.resource.uri)
}
}

const children = (props.children)
? props.children
: UI.label(props.resource, store, podOrigin)

let title = props.title
if (!title) {
title = (typeof children === 'string')
? `View ${children}`
: `View ${UI.label(props.resource, store, podOrigin)}`
}

const anchorProps = {
...props,
title: title,
resource: undefined
}

return (
<a
{...anchorProps}
href={props.resource.uri}
onClick={clickHandler}
>
{children}
</a>
)
}
29 changes: 29 additions & 0 deletions dashboard/datasister-dashboard/context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import $rdf, { IndexedFormula, Fetcher, UpdateManager, NamedNode } from 'rdflib'

export interface DataBrowserContextData {
store: IndexedFormula;
fetcher: Fetcher;
updater: UpdateManager;
podOrigin: string;
webId: string;
loadResource: (resourcePath: string) => void;
};

const defaultContext: DataBrowserContextData = {
podOrigin: document.location.origin,
store: $rdf.graph(),
fetcher: new Fetcher($rdf.graph(), undefined),
updater: new UpdateManager($rdf.graph()),
webId: 'http://example.com',
loadResource: () => undefined
}

/**
* The context allows the data browser to easily access global values
* everywhere in the application.
* Individual Panes, however, should get these values as properties,
* to avoid a hard dependency on the data browser.
* This will allow them to be used as e.g. individual apps or in browser extensions.
*/
export const DataBrowserContext = React.createContext(defaultContext)
11 changes: 11 additions & 0 deletions dashboard/datasister-dashboard/hooks/useWebId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react'
import { DataBrowserContext } from '../context'

/**
* API-compatible with @solid/react's `useWebId`, but fetches it from the context object
*/
export function useWebId () {
const { webId } = React.useContext(DataBrowserContext)

return webId
}
23 changes: 23 additions & 0 deletions dashboard/datasister-dashboard/widgets/Apps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react'
import { DataBrowserContext } from '../context'

export const AppsWidget: React.FC = () => {
const { podOrigin } = React.useContext(DataBrowserContext)

const appLink = (podOrigin)
? `https://pixolid.netlify.com/?idp=${podOrigin}`
: 'https://pixolid.netlify.com/'

return (
<div className="card">
<section className="section">
<h2 className="title">Try this app</h2>
<p className="has-text-centered">
<a href={appLink} title="Open Pixolid">
Pixolid
</a>
</p>
</section>
</div>
)
}
25 changes: 25 additions & 0 deletions dashboard/datasister-dashboard/widgets/Apps/pixolid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions dashboard/datasister-dashboard/widgets/Bookmarks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react'
import $rdf from 'rdflib'
import namespaces from 'solid-namespace'
import { DataBrowserContext } from '../context'
import { useWebId } from '../hooks/useWebId'

const ns = namespaces($rdf)

export const BookmarksWidget: React.FC = () => {
const { store, fetcher, podOrigin } = React.useContext(DataBrowserContext)

const bookmarks = useBookmarks(store, fetcher)

if (!Array.isArray(bookmarks) || bookmarks.length === 0) {
return null
}

return (
<div className="card">
<section className="section">
<h2 className="title">Latest bookmarks</h2>
<div className="content">
<ul>
{bookmarks.slice(0, 5).map((bookmark) => <li key={bookmark.url}><a href={bookmark.url}>{bookmark.title}</a></li>)}
</ul>
</div>
<a className="button is-text" href={`https://vincenttunru.gitlab.io/poddit?idp=${podOrigin || ''}`}>All bookmarks</a>
</section>
</div>
)
}

function useBookmarks (store: $rdf.IndexedFormula, fetcher: $rdf.Fetcher) {
const webId = useWebId()
const [bookmarks, setBookmarks] = React.useState<Array<{ title: string, url: string }>>()

React.useEffect(() => {
if (!webId) {
return
}
getBookmarks(store, fetcher, webId)
.then(setBookmarks)
.catch((e) => console.log('Error fetching bookmarks:', e))
}, [store, fetcher, webId])

return bookmarks
}

async function getBookmarks (store: $rdf.IndexedFormula, fetcher: $rdf.Fetcher, webId: string) {
const profile = $rdf.sym(webId)
const [ publicTypeIndexStatement ] = store.statementsMatching(profile, ns.solid('publicTypeIndex'), null, profile.doc(), true)
const publicTypeIndex = publicTypeIndexStatement.object
await fetcher.load(publicTypeIndex as any as $rdf.NamedNode)
const bookmarkClass = new $rdf.NamedNode('http://www.w3.org/2002/01/bookmark#Bookmark')
const [ bookmarkRegistryStatement ] = store.statementsMatching(null, ns.solid('forClass'), bookmarkClass, publicTypeIndex, true)
const bookmarkRegistry = bookmarkRegistryStatement.subject
const [ bookmarkRegistryInstanceStatement ] = store.statementsMatching(bookmarkRegistry, ns.solid('instance'), null, publicTypeIndex, true)
const bookmarkRegistryInstance = bookmarkRegistryInstanceStatement.object
await fetcher.load(bookmarkRegistryInstance as any as $rdf.NamedNode)
const bookmarkStatements = store.statementsMatching(null, ns.rdf('type'), bookmarkClass, bookmarkRegistryInstance)
return bookmarkStatements.map((statement) => {
const recalls = new $rdf.NamedNode('http://www.w3.org/2002/01/bookmark#recalls')
const bookmarkNode = statement.subject
const [ titleStatement ] = store.statementsMatching(bookmarkNode, ns.dct('title'), null, bookmarkRegistryInstance)
const [ urlStatement ] = store.statementsMatching(bookmarkNode, recalls, null, bookmarkRegistryInstance)
return {
title: titleStatement.object.value,
url: urlStatement.object.value
}
})
}
26 changes: 26 additions & 0 deletions dashboard/datasister-dashboard/widgets/Folder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react'
import $rdf from 'rdflib'
import { ResourceLink } from '../components/ResourceLink'
import { DataBrowserContext } from '../context'

export const FolderWidget: React.FC = () => {
const { podOrigin } = React.useContext(DataBrowserContext)

return (
<div className="card">
<section className="section">
<h2 className="title">Raw data</h2>
<p className="buttons">
<ResourceLink
className="button is-primary is-fullwidth is-medium"
resource={$rdf.sym(`${podOrigin}/public/`)}
>Public data</ResourceLink>
<ResourceLink
className="button is-fullwidth is-small"
resource={$rdf.sym(`${podOrigin}/private/`)}
>Private data</ResourceLink>
</p>
</section>
</div>
)
}
12 changes: 12 additions & 0 deletions dashboard/datasister-dashboard/widgets/Profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';

export const ProfileWidget: React.FC = () => {
return (
<div className="card">
<section className="section">
<h2 className="title">My profile</h2>
<p>No name provided&hellip;</p>
</section>
</div>
);
}
28 changes: 28 additions & 0 deletions dashboard/wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'
import { DataBrowserContextData, DataBrowserContext } from './datasister-dashboard/context';
import { IndexedFormula, Fetcher, UpdateManager } from 'rdflib';
import { Dashboard } from './datasister-dashboard/Dashboard';

interface Props {
store: IndexedFormula,
fetcher: Fetcher,
updater: UpdateManager,
webId: string,
loadResource: (resourcePath: string) => void
}
export const Wrapper: React.FC<Props> = (props) => {
const dataBrowserContext: DataBrowserContextData = {
store: props.store,
fetcher: props.fetcher,
updater: props.updater,
webId: props.webId,
podOrigin: document.location.origin,
loadResource: props.loadResource
}

return (
<DataBrowserContext.Provider value={dataBrowserContext}>
<Dashboard/>
</DataBrowserContext.Provider>
)
}
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ if (typeof window !== 'undefined') {

let register = panes.register

register(require('./markdown/index.tsx').Pane)
register(require('issue-pane'))
register(require('contacts-pane'))

Expand Down Expand Up @@ -127,10 +128,11 @@ register(require('./sharing/sharingPane.js'))

// The internals pane is always (almost?) the last as it is the least user-friendly
register(require('./internalPane.js'))
// The home pane is a 2016 experiment. Always there.

register(require('./profile/profilePane').default) // edit your public profile
register(require('./trustedApplications/trustedApplicationsPane').default) // manage your trusted applications
// The home pane is a 2016 experiment. Always there.
register(require('./home/homePane').default)
register(require('./dashboard/dashboardPane').default)

// ENDS
5 changes: 4 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ module.exports = {
preset: 'ts-jest/presets/js-with-babel',
testEnvironment: 'jsdom',
collectCoverage: true,
setupFilesAfterEnv: [
'@testing-library/react/cleanup-after-each'
],
// For some reason Jest is not measuring coverage without the below option.
// Unfortunately, despite `!(.test)`, it still measures coverage of test files as well:
forceCoverageMatch: ['**/*!(.test).ts'],
forceCoverageMatch: ['**/*!(.test).tsx?'],
// Since we're only measuring coverage for TypeScript (i.e. added with test infrastructure in place),
// we can be fairly strict. However, if you feel that something is not fit for coverage,
// mention why in a comment and mark it as ignored:
Expand Down
Loading