mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-01 11:33:41 +03:00
grid: refactor docket/treaty display
This commit is contained in:
parent
4df4ef56d6
commit
93a9926ad0
131
pkg/grid/src/components/AppInfo.tsx
Normal file
131
pkg/grid/src/components/AppInfo.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { chadIsRunning, Charge, Treaty } from '@urbit/api/docket';
|
||||
import clipboardCopy from 'clipboard-copy';
|
||||
import React, { FC } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Vat } from '@urbit/api/hood';
|
||||
import { Button, PillButton } from './Button';
|
||||
import { Dialog, DialogClose, DialogContent, DialogTrigger } from './Dialog';
|
||||
import { DocketHeader } from './DocketHeader';
|
||||
import { Spinner } from './Spinner';
|
||||
import { VatMeta } from './VatMeta';
|
||||
import useDocketState from '../state/docket';
|
||||
import { getAppHref } from '../state/util';
|
||||
import { addRecentApp } from '../nav/search/Home';
|
||||
import { TreatyMeta } from './TreatyMeta';
|
||||
|
||||
type InstallStatus = 'uninstalled' | 'installing' | 'installed';
|
||||
interface AppInfoProps {
|
||||
docket: Charge | Treaty;
|
||||
vat?: Vat;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function getInstallStatus(docket: Treaty | Charge): 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: Treaty | Charge, vat?: Vat) {
|
||||
if ('chad' in docket) {
|
||||
const { ship, desk } = vat!.arak;
|
||||
return [ship, desk];
|
||||
}
|
||||
const { ship, desk } = docket;
|
||||
return [ship, desk];
|
||||
}
|
||||
|
||||
export const AppInfo: FC<AppInfoProps> = ({ docket, vat, className }) => {
|
||||
const installStatus = getInstallStatus(docket);
|
||||
const [ship, desk] = getRemoteDesk(docket, vat);
|
||||
|
||||
const installApp = async () => {
|
||||
if (installStatus === 'installed') {
|
||||
return;
|
||||
}
|
||||
await useDocketState.getState().installDocket(ship, desk);
|
||||
};
|
||||
const copyApp = () => {
|
||||
clipboardCopy(`web+urbitgraph://app/${ship}/${desk}`);
|
||||
};
|
||||
|
||||
if (!docket) {
|
||||
// TODO: maybe replace spinner with skeletons
|
||||
return (
|
||||
<div className="dialog-inner-container text-black">
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('text-black', className)}>
|
||||
<DocketHeader docket={docket}>
|
||||
<div className="col-span-2 md:col-span-1 flex items-center space-x-4">
|
||||
{installStatus === 'installed' && (
|
||||
<PillButton
|
||||
variant="alt-primary"
|
||||
as="a"
|
||||
href={getAppHref(docket.href)}
|
||||
target={docket.title || '_blank'}
|
||||
onClick={() => addRecentApp(docket)}
|
||||
>
|
||||
Open App
|
||||
</PillButton>
|
||||
)}
|
||||
{installStatus !== 'installed' && (
|
||||
<Dialog>
|
||||
<DialogTrigger as={PillButton} variant="alt-primary">
|
||||
{installStatus === 'installing' ? (
|
||||
<>
|
||||
<Spinner />
|
||||
<span className="sr-only">Installing...</span>
|
||||
</>
|
||||
) : (
|
||||
'Get App'
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent showClose={false} className="max-w-[400px] space-y-6">
|
||||
<h2 className="h4">Install “{docket.title}”</h2>
|
||||
<p className="text-base tracking-tight pr-6">
|
||||
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 as={Button} variant="secondary">
|
||||
Cancel
|
||||
</DialogClose>
|
||||
<DialogClose as={Button} onClick={installApp}>
|
||||
Get “{docket.title}”
|
||||
</DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
<PillButton variant="alt-secondary" onClick={copyApp}>
|
||||
Copy App Link
|
||||
</PillButton>
|
||||
</div>
|
||||
</DocketHeader>
|
||||
{vat ? (
|
||||
<>
|
||||
<hr className="-mx-5 sm:-mx-8" />
|
||||
<VatMeta vat={vat} />
|
||||
</>
|
||||
) : null}
|
||||
{'chad' in docket ? null : (
|
||||
<>
|
||||
<hr className="-mx-5 sm:-mx-8" />
|
||||
<TreatyMeta treaty={docket} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,16 +1,22 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import React, { HTMLProps, ReactNode } from 'react';
|
||||
import { Link, LinkProps } from 'react-router-dom';
|
||||
import { Docket } from '@urbit/api';
|
||||
import { getAppHref } from '../state/util';
|
||||
|
||||
type Sizes = 'xs' | 'small' | 'default';
|
||||
type LinkOrAnchorProps = {
|
||||
[P in keyof LinkProps &
|
||||
keyof HTMLProps<HTMLAnchorElement>]?: LinkProps[P] extends HTMLProps<HTMLAnchorElement>[P]
|
||||
? LinkProps[P]
|
||||
: never;
|
||||
};
|
||||
|
||||
export type AppLinkProps = Omit<LinkProps, 'to'> & {
|
||||
app: Docket;
|
||||
export type AppLinkProps<T extends Docket> = Omit<LinkOrAnchorProps, 'to'> & {
|
||||
app: T;
|
||||
size?: Sizes;
|
||||
selected?: boolean;
|
||||
to?: (app: Docket) => LinkProps['to'];
|
||||
to?: (app: T) => LinkProps['to'] | undefined;
|
||||
};
|
||||
|
||||
const sizeMap: Record<Sizes, string> = {
|
||||
@ -19,24 +25,32 @@ const sizeMap: Record<Sizes, string> = {
|
||||
default: 'w-12 h-12 mr-3 rounded-lg'
|
||||
};
|
||||
|
||||
export const AppLink = ({
|
||||
export const AppLink = <T extends Docket>({
|
||||
app,
|
||||
to,
|
||||
size = 'default',
|
||||
selected = false,
|
||||
className,
|
||||
...props
|
||||
}: AppLinkProps) => {
|
||||
return (
|
||||
<Link
|
||||
to={(to && to(app)) || getAppHref(app.href)}
|
||||
className={classNames(
|
||||
}: AppLinkProps<T>) => {
|
||||
const linkTo = to?.(app);
|
||||
const linkClassnames = classNames(
|
||||
'flex items-center default-ring ring-offset-2 rounded-lg',
|
||||
selected && 'ring-4',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
);
|
||||
const link = (children: ReactNode) =>
|
||||
linkTo ? (
|
||||
<Link to={linkTo} className={linkClassnames} {...props}>
|
||||
{children}
|
||||
</Link>
|
||||
) : (
|
||||
<a href={getAppHref(app.href)} className={linkClassnames} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
return link(
|
||||
<>
|
||||
<div
|
||||
className={classNames('flex-none relative bg-gray-200 rounded-lg', sizeMap[size])}
|
||||
style={{ backgroundColor: app.color }}
|
||||
@ -53,6 +67,6 @@ export const AppLink = ({
|
||||
<p>{app.title}</p>
|
||||
{app.info && size === 'default' && <p className="font-normal">{app.info}</p>}
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -4,13 +4,13 @@ import { MatchItem } from '../nav/Nav';
|
||||
import { useRecentsStore } from '../nav/search/Home';
|
||||
import { AppLink, AppLinkProps } from './AppLink';
|
||||
|
||||
type AppListProps = {
|
||||
apps: Docket[];
|
||||
type AppListProps<T extends Docket> = {
|
||||
apps: T[];
|
||||
labelledBy: string;
|
||||
matchAgainst?: MatchItem;
|
||||
onClick?: (e: MouseEvent<HTMLAnchorElement>, app: Docket) => void;
|
||||
listClass?: string;
|
||||
} & Omit<AppLinkProps, 'app' | 'onClick'>;
|
||||
} & Omit<AppLinkProps<T>, 'app' | 'onClick'>;
|
||||
|
||||
export function appMatches(target: Docket, match?: MatchItem): boolean {
|
||||
if (!match) {
|
||||
@ -21,14 +21,14 @@ export function appMatches(target: Docket, match?: MatchItem): boolean {
|
||||
return target.title === matchValue; // TODO: need desk name or something || target.href === matchValue;
|
||||
}
|
||||
|
||||
export const AppList = ({
|
||||
export const AppList = <T extends Docket>({
|
||||
apps,
|
||||
labelledBy,
|
||||
matchAgainst,
|
||||
onClick,
|
||||
listClass = 'space-y-8',
|
||||
...props
|
||||
}: AppListProps) => {
|
||||
}: AppListProps<T>) => {
|
||||
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
|
||||
const selected = useCallback((app: Docket) => appMatches(app, matchAgainst), [matchAgainst]);
|
||||
|
||||
|
@ -1,14 +1,16 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { capitalize } from 'lodash-es';
|
||||
|
||||
interface AttributeProps {
|
||||
attr: string;
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Attribute = ({ attr, children, title }: AttributeProps) => (
|
||||
<div className="h4">
|
||||
export const Attribute = ({ attr, children, title, className }: AttributeProps) => (
|
||||
<div className={cn('h4', className)}>
|
||||
<h2 className="mb-2 text-gray-500">{title || capitalize(attr)}</h2>
|
||||
<p className="font-mono">{children}</p>
|
||||
</div>
|
||||
|
@ -3,18 +3,19 @@ import { Docket } from '@urbit/api/docket';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface DocketImageProps extends Pick<Docket, 'color' | 'image'> {
|
||||
small?: boolean;
|
||||
className?: string;
|
||||
sizing: 'small' | 'full';
|
||||
}
|
||||
|
||||
export function DocketImage({ color, image, className = '', small = false }: DocketImageProps) {
|
||||
const sizing = small
|
||||
? 'w-4 h-4 md:w-6 md:h-6 rounded-md'
|
||||
export function DocketImage({ color, image, className = '', sizing = 'full' }: DocketImageProps) {
|
||||
const sizingClass =
|
||||
sizing === 'full'
|
||||
? 'w-full h-full md:w-full md:h-full rounded-md'
|
||||
: 'w-12 h-12 md:w-20 md:h-20 rounded-xl';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(sizing, `flex-none relative bg-gray-200`, className)}
|
||||
className={cn(sizingClass, `flex-none relative bg-gray-200`, className)}
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
{image && (
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Treaty } from '@urbit/api';
|
||||
import { Treaty, daToDate } from '@urbit/api';
|
||||
|
||||
import moment from 'moment';
|
||||
import { Attribute } from './Attribute';
|
||||
|
||||
const meta = ['license', 'website', 'version'] as const;
|
||||
@ -8,13 +9,14 @@ const meta = ['license', 'website', 'version'] as const;
|
||||
export function TreatyMeta(props: { treaty: Treaty }) {
|
||||
const { treaty } = props;
|
||||
const { desk, ship, cass } = treaty;
|
||||
console.log(cass.da);
|
||||
return (
|
||||
<div className="mt-5 sm:mt-8 space-y-5 sm:space-y-8">
|
||||
<Attribute title="Developer Desk" attr="desk">
|
||||
{ship}/{desk}
|
||||
</Attribute>
|
||||
<Attribute title="Last Software Update" attr="case">
|
||||
{JSON.stringify(cass)}
|
||||
{moment(daToDate(cass.da as unknown as string)).format('YYYY.MM.DD')}
|
||||
</Attribute>
|
||||
{meta.map((d) => (
|
||||
<Attribute key={d} attr={d}>
|
||||
|
33
pkg/grid/src/components/VatMeta.tsx
Normal file
33
pkg/grid/src/components/VatMeta.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { Vat } from '@urbit/api/hood';
|
||||
|
||||
import { Attribute } from './Attribute';
|
||||
|
||||
export function VatMeta(props: { vat: Vat }) {
|
||||
const { vat } = props;
|
||||
const { desk, arak, cass, hash } = vat;
|
||||
const { desk: foreignDesk, ship, next } = arak;
|
||||
const pluralUpdates = next.length !== 1;
|
||||
return (
|
||||
<div className="mt-5 sm:mt-8 space-y-5 sm:space-y-8">
|
||||
<Attribute title="Developer Desk" attr="desk">
|
||||
{ship}/{foreignDesk}
|
||||
</Attribute>
|
||||
<Attribute title="Last Software Update" attr="case">
|
||||
{moment(cass.da).format('YYYY.MM.DD')}
|
||||
</Attribute>
|
||||
<Attribute title="Desk Hash" attr="hash">
|
||||
{hash}
|
||||
</Attribute>
|
||||
<Attribute title="Installed into" attr="local-desk">
|
||||
%{desk}
|
||||
</Attribute>
|
||||
{next.length > 0 ? (
|
||||
<Attribute attr="next" title="Pending Updates">
|
||||
{next.length} update{pluralUpdates ? 's are' : ' is'} pending a System Update
|
||||
</Attribute>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user