grid: lag, blocking from kiln

This commit is contained in:
Liam Fitzgerald 2021-08-25 17:45:20 +10:00
parent 93a9926ad0
commit 63515c573a
9 changed files with 123 additions and 156 deletions

View File

@ -40,6 +40,8 @@ export const useLeapStore = create<LeapStore>((set) => ({
})
}));
window.leap = useLeapStore.getState;
export type MenuState =
| 'closed'
| 'search'

View File

@ -1,105 +1,23 @@
import React, { FC, useEffect, useState } from 'react';
import cn from 'classnames';
import React, { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useLeapStore } from './Nav';
import { useBlockers, useLag } from '../state/kiln';
import { useCharge } from '../state/docket';
import { DocketImage } from '../components/DocketImage';
import api from '../state/api';
import { kilnSuspend } from '../../../npm/api/hood';
import { Button } from '../components/Button';
import { useHarkStore } from '../state/hark';
import { Notification } from '../state/hark-types';
import { BasicNotification } from './notifications/BasicNotification';
import { SystemNotification } from './notifications/SystemNotification';
interface INotification {
title: string;
body: string;
actions: {
title: string;
role: 'primary' | 'destructive' | 'none';
link: string;
}[];
}
interface NotificationProps {
notification?: INotification;
read?: boolean;
className?: string;
children?: React.ReactNode;
}
const Notification: FC<NotificationProps> = ({ className, children }) => (
<div className={cn('rounded-md flex flex-col p-4', className)}>{children}</div>
);
const LagNotification = () => (
<Notification read={false} className="bg-orange-100">
<p className="text-black leading-normal">
The runtime of this ship is out of date, and preventing a kernel upgrade. Please upgrade to
the latest runtime version. If you are hosted, please contact your hosting provider
</p>
</Notification>
);
const DeskIcon = ({ desk }) => {
const { title, image, color } = useCharge(desk);
return (
<div className="flex items-center space-x-2">
<DocketImage small color={color} image={image} />
<p className="text-black font-medium">{title}</p>
</div>
);
};
interface BlockNotificationProps {
desks: string[];
}
const BlockNotification: React.FC<BlockNotificationProps> = ({ desks }) => {
const count = desks.length;
const [dismissed, setDismissed] = useState(false);
const onArchive = async () => {
await Promise.all(desks.map((d) => api.poke(kilnSuspend(d))));
};
if (dismissed) {
return null;
}
return (
<Notification className="bg-orange-100 flex-col space-y-4 p-6">
<p className="text-black"> The following {desks.length} apps blocked a System Update: </p>
<div className="flex flex-col space-y-4">
{desks.map((desk) => (
<DeskIcon key={desk} desk={desk} />
))}
</div>
<p className="text-black">
In order to proceed with the System Update, youll need to temporarily archive these apps,
which will render them unusable, but with data intact.
<br />
<br />
Archived apps will automatically un-archive and resume operation when their developer
provides an app update.
</p>
<div className="flex space-x-4">
<button type="button" onClick={() => setDismissed(true)}>
Dismiss
</button>
<button type="button" onClick={onArchive}>
Archive {count} apps and System Update
</button>
</div>
</Notification>
);
};
import {
BaseBlockedNotification,
RuntimeLagNotification
} from './notifications/SystemNotification';
function renderNotification(notification: Notification, key: string) {
if (notification.type === 'system-updates-blocked') {
return <SystemNotification key={key} notification={notification} />;
return <BaseBlockedNotification key={key} notification={notification} />;
}
if (notification.type === 'runtime-lag') {
return <RuntimeLagNotification key={key} />;
}
return <BasicNotification key={key} notification={notification} />;
}
@ -109,22 +27,31 @@ const Empty = () => (
</section>
);
function getSystemNotifications(lag: boolean, blockers: string[]) {
const nots = [] as Notification[];
if (lag) {
nots.push({ type: 'runtime-lag' });
}
if (blockers.length > 0) {
nots.push({ type: 'system-updates-blocked', desks: blockers });
}
return nots;
}
export const Notifications = () => {
const select = useLeapStore((s) => s.select);
const notifications = useHarkStore((s) => s.notifications);
const hasNotifications = notifications.length > 0;
const blockers = useBlockers();
const lag = useLag();
const systemNotifications = getSystemNotifications(lag, blockers);
const hasNotifications = notifications.length > 0 || systemNotifications.length > 0;
useEffect(() => {
select('Notifications');
}, []);
const blockers = useBlockers();
const lag = useLag();
return (
<div className="p-4 md:p-8">
{lag && <LagNotification />}
{blockers.length > 0 ? <BlockNotification desks={blockers} /> : null}
<header className="space-x-2 mb-8">
<Button variant="secondary" className="py-1.5 px-6 rounded-full">
Mark All as Read
@ -141,8 +68,11 @@ export const Notifications = () => {
{!hasNotifications && <Empty />}
{hasNotifications && (
<section className="min-h-[480px] text-gray-400 space-y-2">
<section className="min-h-[480px] text-gray-400 space-y-2 overflow-y-auto">
{notifications.map((n, index) => renderNotification(n, index.toString()))}
{systemNotifications.map((n, index) =>
renderNotification(n, (notifications.length + index).toString())
)}
</section>
)}
</div>

View File

@ -1,6 +1,6 @@
import React from 'react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import { AppInfo } from './search/AppInfo';
import { TreatyInfo } from './search/TreatyInfo';
import { Apps } from './search/Apps';
import { Home } from './search/Home';
import { Providers } from './search/Providers';
@ -12,7 +12,7 @@ type SearchProps = RouteComponentProps<{
export const Search = ({ match }: SearchProps) => {
return (
<Switch>
<Route path={`${match.path}/:ship/apps/:host/:desk`} component={AppInfo} />
<Route path={`${match.path}/:ship/apps/:host/:desk`} component={TreatyInfo} />
<Route path={`${match.path}/:ship/apps`} component={Apps} />
<Route path={`${match.path}/:ship`} component={Providers} />
<Route path={`${match.path}`} component={Home} />

View File

@ -1,30 +1,58 @@
import { pick } from 'lodash-es';
import React, { useCallback } from 'react';
import { kilnSuspend } from '@urbit/api/hood';
import { AppList } from '../../components/AppList';
import { Button } from '../../components/Button';
import { Dialog, DialogClose, DialogContent, DialogTrigger } from '../../components/Dialog';
import { Elbow } from '../../components/icons/Elbow';
import api from '../../state/api';
import { useCharges } from '../../state/docket';
import { SystemNotification as SystemNotificationType } from '../../state/hark-types';
import {
BaseBlockedNotification as BaseBlockedNotificationType,
} from '../../state/hark-types';
import { NotificationButton } from './NotificationButton';
interface SystemNotificationProps {
notification: SystemNotificationType;
interface BaseBlockedNotificationProps {
notification: BaseBlockedNotificationType;
}
export const SystemNotification = ({ notification }: SystemNotificationProps) => {
const keys = notification.charges;
export const RuntimeLagNotification = () => (
<section
className="notification pl-12 space-y-2 text-black bg-orange-50"
aria-labelledby="runtime-lag"
>
<header id="system-updates-blocked" className="relative -left-8 space-y-2">
<div className="flex space-x-2">
<span className="inline-block w-6 h-6 bg-orange-500 rounded-full" />
<span className="font-medium">Landscape</span>
</div>
<div className="flex space-x-2">
<Elbow className="w-6 h-6 text-gray-300" />
<h2 id="runtime-lag">The runtime blocked a System Update</h2>
</div>
</header>
<div className="space-y-6">
<p>
In order to proceed with the System Update, youll need to upgrade the runtime. If you are
using a hosted ship, you should contact your hosting provider.
</p>
</div>
</section>
);
export const BaseBlockedNotification = ({ notification }: BaseBlockedNotificationProps) => {
const { desks } = notification;
const charges = useCharges();
const blockedCharges = Object.values(pick(charges, keys));
const blockedCharges = Object.values(pick(charges, desks));
const count = blockedCharges.length;
const handlePauseOTAs = useCallback(() => {
console.log('pause updates');
}, []);
const handlePauseOTAs = useCallback(() => {}, []);
const handleArchiveApps = useCallback(() => {
console.log('archive apps');
}, []);
const handleArchiveApps = useCallback(async () => {
await Promise.all(desks.map(d => api.poke(kilnSuspend(d))));
// TODO: retrigger OTA?
}, [desks]);
return (
<section

View File

@ -25,6 +25,7 @@ export const Apps = ({ match }: AppsProps) => {
}));
const provider = match?.params.ship;
const treaties = useAllyTreaties(provider);
console.log(treaties);
const results = useMemo(() => {
if (!treaties) {
return undefined;
@ -46,12 +47,14 @@ export const Apps = ({ match }: AppsProps) => {
const count = results?.length;
useEffect(() => {
const { fetchAllyTreaties } = useDocketState.getState();
fetchAllyTreaties(provider);
select(
<>
Apps by <ShipName name={provider} className="font-mono" />
</>
);
}, []);
}, [provider]);
useEffect(() => {
if (results) {
@ -82,7 +85,7 @@ export const Apps = ({ match }: AppsProps) => {
apps={results}
labelledBy="developed-by"
matchAgainst={selectedMatch}
to={(app) => `${match?.path.replace(':ship', provider)}/${slugify(app.base)}`}
to={(app) => `${match?.path.replace(':ship', provider)}/${app.ship}/${slugify(app.desk)}`}
/>
)}
<p>That&apos;s it!</p>

View File

@ -58,9 +58,19 @@ export const useRecentsStore = create<RecentsStore>(
)
);
window.recents = useRecentsStore.getState;
export function addRecentDev(dev: Provider) {
return useRecentsStore.getState().addRecentDev(dev);
}
export function addRecentApp(docket: Docket) {
return useRecentsStore.getState().addRecentApp(docket);
}
export const Home = () => {
const selectedMatch = useLeapStore((state) => state.selectedMatch);
const { recentApps, recentDevs, addRecentApp, addRecentDev } = useRecentsStore();
const { recentApps, recentDevs } = useRecentsStore();
const charges = useCharges();
const groups = charges?.groups;
const zod = { shipName: '~zod' };

View File

@ -1,27 +1,17 @@
import { chadIsRunning } from '@urbit/api/docket';
import clipboardCopy from 'clipboard-copy';
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Button, PillButton } from '../../components/Button';
import { Dialog, DialogClose, DialogContent, DialogTrigger } from '../../components/Dialog';
import { DocketHeader } from '../../components/DocketHeader';
import { AppInfo } from '../../components/AppInfo';
import { ShipName } from '../../components/ShipName';
import { Spinner } from '../../components/Spinner';
import { TreatyMeta } from '../../components/TreatyMeta';
import useDocketState, { useCharges, useTreaty } from '../../state/docket';
import { getAppHref } from '../../state/util';
import { useCharge, useTreaty } from '../../state/docket';
import { useVat } from '../../state/kiln';
import { useLeapStore } from '../Nav';
import { useRecentsStore } from './Home';
export const AppInfo = () => {
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
export const TreatyInfo = () => {
const select = useLeapStore((state) => state.select);
const { ship, host, desk } = useParams<{ ship: string; host: string; desk: string }>();
const treaty = useTreaty(host, desk);
const charges = useCharges();
const charge = (charges || {})[desk];
const installed = charge && chadIsRunning(charge.chad);
const installing = charge && 'install' in charge.chad;
const vat = useVat(desk);
const charge = useCharge(desk);
useEffect(() => {
select(
@ -31,13 +21,6 @@ export const AppInfo = () => {
);
}, [treaty?.title]);
const installApp = async () => {
await useDocketState.getState().installDocket(ship, desk);
};
const copyApp = () => {
clipboardCopy(`web+urbitgraph://app/${ship}/${desk}`);
};
if (!treaty) {
// TODO: maybe replace spinner with skeletons
return (
@ -46,8 +29,11 @@ export const AppInfo = () => {
</div>
);
}
return <AppInfo className="dialog-inner-container" docket={vat ? charge : treaty} vat={vat} />;
/*
return (
<div className="dialog-inner-container text-black">
<DocketHeader docket={treaty}>
<div className="col-span-2 md:col-span-1 flex items-center space-x-4">
@ -100,4 +86,5 @@ export const AppInfo = () => {
<TreatyMeta treaty={treaty} />
</div>
);
*/
};

View File

@ -1,14 +1,13 @@
import { map, omit } from 'lodash-es';
import React, { FunctionComponent, useEffect } from 'react';
import { Route, RouteComponentProps } from 'react-router-dom';
import { KilnDiff } from '@urbit/api/hood';
import { MenuState, Nav } from '../nav/Nav';
import useDocketState, { useCharges } from '../state/docket';
import { useKilnState } from '../state/kiln';
import { RemoveApp } from '../tiles/RemoveApp';
import { SuspendApp } from '../tiles/SuspendApp';
import { Tile } from '../tiles/Tile';
import api from '../state/api';
import { TileInfo } from '../tiles/TileInfo';
type GridProps = RouteComponentProps<{
menu?: MenuState;
@ -25,22 +24,6 @@ export const Grid: FunctionComponent<GridProps> = ({ match }) => {
fetchAllies();
fetchVats();
fetchLag();
api
.subscribe({
app: 'hood',
path: '/kiln/vats',
event: (data: KilnDiff) => {
console.log(data);
},
err: () => {},
quit: () => {}
})
.catch((e) => {
console.log(e);
})
.then((r) => {
console.log(r);
});
}, []);
return (
@ -59,6 +42,9 @@ export const Grid: FunctionComponent<GridProps> = ({ match }) => {
))}
</div>
)}
<Route exact path="/app/:desk">
<TileInfo />
</Route>
<Route exact path="/app/:desk/suspend">
<SuspendApp />
</Route>

View File

@ -0,0 +1,21 @@
import React from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { Dialog, DialogContent } from '../components/Dialog';
import { AppInfo } from '../components/AppInfo';
import { useCharge } from '../state/docket';
import { useVat } from '../state/kiln';
export const TileInfo = () => {
const { desk } = useParams<{ desk: string }>();
const { push } = useHistory();
const charge = useCharge(desk);
const vat = useVat(desk);
return (
<Dialog open onOpenChange={(open) => !open && push('/')}>
<DialogContent>
<AppInfo vat={vat} docket={charge} />
</DialogContent>
</Dialog>
);
};