Skip to content

Commit

Permalink
perms: refactor install flow to prompt user
Browse files Browse the repository at this point in the history
also:
- extract functions to hooks in docket
- use routes to render perm dialog
- add fake seal placeholder
- iterate on
  • Loading branch information
tomholford committed Jun 14, 2023
1 parent b19c60a commit 1ede094
Show file tree
Hide file tree
Showing 10 changed files with 336 additions and 166 deletions.
93 changes: 11 additions & 82 deletions ui/src/components/AppInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,37 @@
import { chadIsRunning, Pike, Treaty } from '@/gear';
import { Pike } from '@/gear';
import clipboardCopy from 'clipboard-copy';
import React, { FC, useCallback, useState } from 'react';
import cn from 'classnames';
import { Button, PillButton } from './Button';
import * as Dialog from '@radix-ui/react-dialog';
import { DialogClose, DialogContent, DialogTrigger } from './Dialog';
import { PillButton } from './Button';
import { DocketHeader } from './DocketHeader';
import { Spinner } from './Spinner';
import { PikeMeta } from './PikeMeta';
import useDocketState, { ChargeWithDesk, useTreaty } from '../state/docket';
import { App, useInstallStatus, useRemoteDesk, useTreaty } from '../state/docket';
import { getAppHref, getAppName } from '@/logic/utils';
import { addRecentApp } from '../nav/search/Home';
import { TreatyMeta } from './TreatyMeta';
import { useHistory, useParams } from 'react-router-dom';

type InstallStatus = 'uninstalled' | 'installing' | 'installed';

type App = ChargeWithDesk | Treaty;
interface AppInfoProps {
docket: App;
pike?: Pike;
treatyInfoShip?: string;
className?: string;
}

function getInstallStatus(docket: App): InstallStatus {
if (!('chad' in docket)) {
return 'uninstalled';
}
if (chadIsRunning(docket.chad)) {
return 'installed';
}
if ('install' in docket.chad) {
return 'installing';
}
return 'uninstalled';
}

function getRemoteDesk(docket: App, pike?: Pike, treatyInfoShip?: string) {
if (pike && pike.sync) {
return [pike.sync.ship, pike.sync.desk];
}
if ('chad' in docket) {
return [treatyInfoShip ?? '', docket.desk];
}
const { ship, desk } = docket;
return [ship, desk];
}

export const AppInfo: FC<AppInfoProps> = ({
docket,
pike,
className,
treatyInfoShip,
}) => {
const installStatus = getInstallStatus(docket);
const [ship, desk] = getRemoteDesk(docket, pike, treatyInfoShip);
const [ship, desk] = useRemoteDesk(docket, pike, treatyInfoShip);
const publisher = pike?.sync?.ship ?? ship;
const [copied, setCopied] = useState(false);
const treaty = useTreaty(ship, desk);

const installApp = async () => {
if (installStatus === 'installed') {
return;
}
await useDocketState.getState().installDocket(ship, desk);
};
const { push } = useHistory();
const installStatus = useInstallStatus(docket);
const { host } = useParams<{ host: string }>();

const copyApp = useCallback(() => {
setCopied(true);
Expand Down Expand Up @@ -103,47 +70,9 @@ export const AppInfo: FC<AppInfoProps> = ({
</PillButton>
)}
{installStatus !== 'installed' && (
<Dialog.Root>
<DialogTrigger asChild>
<PillButton variant="alt-primary" disabled={installing}>
{installing ? (
<>
<Spinner />
<span className="sr-only">Installing...</span>
</>
) : (
'Get App'
)}
</PillButton>
</DialogTrigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed top-0 bottom-0 left-0 right-0 z-[60] transform-gpu bg-black opacity-30" />
<DialogContent
showClose={false}
className="space-y-6"
containerClass="w-full max-w-md z-[70]"
>
<h2 className="h4">
Install &ldquo;{getAppName(docket)}&rdquo;
</h2>
<p className="pr-6 tracking-tight">
This application will be able to view and interact with the
contents of your Urbit. Only install if you trust the
developer.
</p>
<div className="flex space-x-6">
<DialogClose asChild>
<Button variant="secondary">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={installApp}>
Get &ldquo;{getAppName(docket)}&rdquo;
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog.Portal>
</Dialog.Root>
<PillButton variant='alt-primary' onClick={() => push(`/search/${ship}/apps/${host}/${desk}/permissions`)}>
Get &ldquo;{getAppName(docket)}&rdquo;
</PillButton>
)}
<PillButton variant="alt-secondary" onClick={copyApp}>
{!copied && 'Copy App Link'}
Expand Down
53 changes: 53 additions & 0 deletions ui/src/components/Permissions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { Dialog, DialogContent } from './Dialog';
import { PermissionsDialogInner } from '@/permissions/PermissionsDialog';
import { Spinner } from '@/components/Spinner';
import usePermissions from '@/permissions/usePermissions';

export const Permissions = () => {
const { push } = useHistory();
const {
appName,
desk,
docket,
installStatus,
passport,
pike,
ship,
} = usePermissions();

const onInstall = useCallback(async () => {
if (installStatus === 'installed') {
return;
}

// await useDocketState.getState().approvePermissions(ship, desk);

// await useDocketState.getState().installDocket(ship, desk);
console.log(`installing ${ship}/${desk}...`);
push(`/app/${desk}`);
}, []);

return (
<Dialog open onOpenChange={(open) => !open && push('/')}>
<DialogContent
showClose={false}
containerClass="w-full max-w-xl z-[70]"
>
{
(docket || pike) && passport ? (
<PermissionsDialogInner
appName={appName}
passport={passport}
onInstall={onInstall}
/>) : (
<div className="dialog-inner-container flex justify-center text-black">
<Spinner className="h-10 w-10" />
</div>
)
}
</DialogContent>
</Dialog>
);
};
1 change: 0 additions & 1 deletion ui/src/nav/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
Route,
Switch,
useHistory,
useRouteMatch,
} from 'react-router-dom';
import create from 'zustand';
import { Avatar } from '../components/Avatar';
Expand Down
5 changes: 5 additions & 0 deletions ui/src/nav/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Apps } from './search/Apps';
import { Home } from './search/Home';
import { Providers } from './search/Providers';
import { ErrorAlert } from '../components/ErrorAlert';
import { Permissions } from '../components/Permissions'; // TODO: use this route instead of the other (so the hooks can resolve from URL params)

type SearchProps = RouteComponentProps<{
query?: string;
Expand All @@ -15,6 +16,10 @@ export const Search = ({ match, history }: SearchProps) => {
return (
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => history.push('/leap/search')}>
<Switch>
<Route
path={[`${match.path}/direct/apps/:host/:desk/permissions`, `${match.path}/:ship/apps/:host/:desk/permissions`]}
component={Permissions}
/>
<Route
path={[`${match.path}/direct/apps/:host/:desk`, `${match.path}/:ship/apps/:host/:desk`]}
component={TreatyInfo}
Expand Down
102 changes: 26 additions & 76 deletions ui/src/permissions/PermissionsDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
import React, { Dispatch, SetStateAction, useState } from 'react';
import React, { useState } from 'react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { Dialog, DialogClose, DialogContent } from '@/components/Dialog';
import { DialogClose } from '@/components/Dialog';
import { Button } from '@/components/Button';
import { Passport } from '@/gear';
import WarningBanner from './WarningBanner';
import { ChevronDown16Icon } from '@/components/icons/ChevronDown16Icon';
import SummaryRow from './SummaryRow';
import { fakeSeal } from './temp';

type ViewMode = 'Summary' | 'Source';

interface PermissionsDialogInnerProps {
appName: string;
passport: Passport;
setViewMode: Dispatch<SetStateAction<ViewMode>>;
viewMode: ViewMode;
onInstall: () => void;
}

export function PermissionsDialogInner({
appName,
passport,
setViewMode,
viewMode,
onInstall,
}: PermissionsDialogInnerProps) {
const [viewMode, setViewMode] = useState<ViewMode>('Summary');
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
// TODO: use the passport to render the permissions Summary
const showWarning = passport.sys.length > 0;

return (
<div className="space-y-6 my-6">
<div className="space-y-6">
<section className='flex justify-between items-center'>
<span className="h4">
"{appName}" Requires Permissions
Expand All @@ -53,89 +52,40 @@ export function PermissionsDialogInner({
{
viewMode === 'Summary' ? (
<div className="space-y-5">
{/* TODO: render app passport bucket */}
{
passport.sys.map(p => {
[...passport.sys, ...passport.any, ...passport.new, ...passport.rad].map(p => {
return p.kind.pes.map((pe, i) => {
return <SummaryRow key={i} summary={pe} />
})
})
}
</div>
) : (
<div>
{/* A code block */}
Source
<div className="bg-gray-100 rounded-md p-4">
<pre className="text-xs font-mono text-gray-600">
{/* TODO: use real seal from scry; style to match wireframe */}
{fakeSeal}
</pre>
</div>
)
}
</section>

{viewMode === 'Summary' && showWarning ? (
<section>
<WarningBanner count={passport.sys.length} />
</section>
<WarningBanner count={passport.sys.length} />
) : null}
</div>
);
}

export default function PermissionsDialog() {
// const passport = usePassport({ ship, desk });

const [viewMode, setViewMode] = useState<ViewMode>('Summary');

const appName = 'Groups';
const passport: Passport = {
rad: [],
sys: [
{
kind: {
nom: 'write',
pes: [
{
desc: 'Access network keys or passwords',
have: "nil",
warn: "This app can impersonate you on the network",
pers: [
{
name: 'write',
vane: 'jael',
tail: null,
}
]
}
]
}
}
],
any: [],
new: [],
app: [],
};

return (
<Dialog open>
<DialogContent
showClose={false}
containerClass="w-full max-w-md z-[70]"
>
<PermissionsDialogInner
appName={appName}
passport={passport}
setViewMode={setViewMode}
viewMode={viewMode}
/>
<div className="flex justify-end space-x-2">
<DialogClose asChild>
<Button variant="light-secondary">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={() => { }}>
Grant & Install
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
<div className="flex justify-end space-x-2">
<DialogClose asChild>
<Button onClick={() => history.back()} variant="light-secondary">Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={onInstall}>
Grant & Install
</Button>
</DialogClose>
</div>
</div>
);
}
Loading

0 comments on commit 1ede094

Please sign in to comment.