vitals: add new connection check to frontend and improve treaty fetch

This commit is contained in:
Hunter Miller 2023-06-22 16:08:48 -05:00
parent ff3c55e6a2
commit 733ce51ea8
7 changed files with 371 additions and 77 deletions

2
desk/mar/run-check.hoon Normal file
View File

@ -0,0 +1,2 @@
/= mark /mar/ship
mark

View File

@ -26,12 +26,11 @@
:- ['complete' [%s -.p.status.result]] :- ['complete' [%s -.p.status.result]]
?+ -.p.status.result ~ ?+ -.p.status.result ~
%no-our-planet ['last-contact' (time:enjs last-contact.p.status.result)]~ %no-our-planet ['last-contact' (time:enjs last-contact.p.status.result)]~
%no-our-sponsor ['last-contact' (time:enjs last-contact.p.status.result)]~
%no-our-galaxy ['last-contact' (time:enjs last-contact.p.status.result)]~ %no-our-galaxy ['last-contact' (time:enjs last-contact.p.status.result)]~
%no-sponsor-hit ['ship' (ship:enjs ship.p.status.result)]~ %no-sponsor-hit ['ship' (ship:enjs ship.p.status.result)]~
%no-sponsor-miss ['ship' (ship:enjs ship.p.status.result)]~ %no-sponsor-miss ['ship' (ship:enjs ship.p.status.result)]~
%no-their-galaxy ['last-contact' (time:enjs last-contact.p.status.result)]~ %no-their-galaxy ['last-contact' (time:enjs last-contact.p.status.result)]~
%crash ['crash' (tang:enjs tang.p.status.result)]~ %crash ['crash' a+(turn tang.p.status.result tank:enjs)]~
== ==
== ==
== ==

4
ui/package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "landscape", "name": "landscape",
"version": "0.0.0", "version": "1.11.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "landscape", "name": "landscape",
"version": "0.0.0", "version": "1.11.0",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.348.0", "@aws-sdk/client-s3": "^3.348.0",
"@aws-sdk/s3-request-presigner": "^3.348.0", "@aws-sdk/s3-request-presigner": "^3.348.0",

View File

@ -0,0 +1,79 @@
import cn from 'classnames';
import React from 'react';
import {
ConnectionCompleteStatus,
ConnectionPendingStatus,
ConnectionStatus,
} from '@/state/vitals';
import Bullet16Icon from './icons/Bullet16Icon';
interface ShipConnectionProps {
ship: string;
status?: ConnectionStatus;
className?: string;
}
function getCompletedText(status: ConnectionCompleteStatus, ship: string) {
switch (status.complete) {
case 'no-data':
return 'No connection data';
case 'yes':
return 'Connected';
case 'no-dns':
return 'Unable to connect to DNS';
case 'no-our-planet':
return 'Unable to reach our planet';
case 'no-our-galaxy':
return 'Unable to reach our galaxy';
case 'no-their-galaxy':
return `Unable to reach ${ship}'s galaxy`;
case 'no-sponsor-miss':
return `${ship}'s sponsor can't reach them`;
case 'no-sponsor-hit':
return `${ship}'s sponsor can reach them, but we can't`;
default:
return 'Unable to connect';
}
}
function getPendingText(status: ConnectionPendingStatus, ship: string) {
switch (status.pending) {
case 'trying-dns':
return 'Checking DNS';
case 'trying-local':
return 'Checking our galaxy';
case 'trying-target':
return `Checking ${ship}`;
case 'trying-sponsor':
return `Checking ${ship}'s sponsors (~${(status as any).ship})`;
default:
return 'Checking connection...';
}
}
function getConnectionColor(status?: ConnectionStatus) {
if (!status || 'pending' in status) {
return 'text-gray-400';
}
return status.complete === 'yes' ? 'text-green-300' : 'text-red-400';
}
export function ShipConnection({
status,
ship,
className,
}: ShipConnectionProps) {
return (
<span className={cn('flex text-base font-semibold', className)}>
<Bullet16Icon
className={cn('-ml-1 h-4 w-4', getConnectionColor(status))}
/>{' '}
{!status
? 'No connection data'
: 'pending' in status
? getPendingText(status, ship)
: getCompletedText(status, ship)}
</span>
);
}

View File

@ -8,6 +8,8 @@ import { useAppSearchStore } from '../Nav';
import { AppList } from '../../components/AppList'; import { AppList } from '../../components/AppList';
import { addRecentDev } from './Home'; import { addRecentDev } from './Home';
import { Spinner } from '../../components/Spinner'; import { Spinner } from '../../components/Spinner';
import { ShipConnection } from '@/components/ShipConnection';
import { pluralize } from '@/logic/utils';
type AppsProps = RouteComponentProps<{ ship: string }>; type AppsProps = RouteComponentProps<{ ship: string }>;
@ -15,10 +17,12 @@ export const Apps = ({ match }: AppsProps) => {
const { searchInput, selectedMatch, select } = useAppSearchStore((state) => ({ const { searchInput, selectedMatch, select } = useAppSearchStore((state) => ({
searchInput: state.searchInput, searchInput: state.searchInput,
select: state.select, select: state.select,
selectedMatch: state.selectedMatch selectedMatch: state.selectedMatch,
})); }));
const provider = match?.params.ship; const provider = match?.params.ship;
const { treaties, status } = useAllyTreaties(provider); const { treaties, status, connection, awaiting } = useAllyTreaties(provider);
console.log(status);
const [showConnection, setShowConnection] = React.useState(false);
useEffect(() => { useEffect(() => {
if (provider) { if (provider) {
@ -26,6 +30,16 @@ export const Apps = ({ match }: AppsProps) => {
} }
}, [provider]); }, [provider]);
useEffect(() => {
const timeout = setTimeout(() => {
setShowConnection(true);
}, 700);
return () => {
clearTimeout(timeout);
};
}, []);
const results = useMemo(() => { const results = useMemo(() => {
if (!treaties) { if (!treaties) {
return undefined; return undefined;
@ -47,7 +61,8 @@ export const Apps = ({ match }: AppsProps) => {
const count = results?.length; const count = results?.length;
const getAppPath = useCallback( const getAppPath = useCallback(
(app: Treaty) => `${match?.path.replace(':ship', provider)}/${app.ship}/${app.desk}`, (app: Treaty) =>
`${match?.path.replace(':ship', provider)}/${app.ship}/${app.desk}`,
[match] [match]
); );
@ -66,44 +81,73 @@ export const Apps = ({ match }: AppsProps) => {
url: getAppPath(r), url: getAppPath(r),
openInNewTab: false, openInNewTab: false,
value: r.desk, value: r.desk,
display: r.title display: r.title,
})) })),
}); });
} }
}, [results]); }, [results]);
const showNone = const showLoader =
status === 'error' || ((status === 'success' || status === 'initial') && results?.length === 0); status === 'loading' || status === 'initial' || status === 'awaiting';
return ( return (
<div className="dialog-inner-container md:px-6 md:py-8 h4 text-gray-400"> <div className="dialog-inner-container h4 text-gray-400 md:px-6 md:py-8">
{status === 'loading' && ( {showLoader && (
<span className="mb-3"> <div className="mb-3 flex items-start">
<Spinner className="w-7 h-7 mr-3" /> Finding software... <Spinner className="mr-3 h-7 w-7 flex-none" />
</span> <div className="flex flex-1 flex-col">
<span>
{status === 'awaiting'
? `${awaiting} ${pluralize(
'app',
awaiting
)} found, waiting for entries...`
: 'Finding software...'}
</span>
{showConnection && (
<ShipConnection ship={provider} status={connection?.status} />
)}
</div>
</div>
)} )}
{results && results.length > 0 && ( {(status === 'partial' || status === 'finished') &&
<> results &&
(results.length > 0 ? (
<>
<div id="developed-by">
<h2 className="mb-3">
Software developed by{' '}
<ShipName name={provider} className="font-mono" />
</h2>
<p>
{count} result{count === 1 ? '' : 's'}
</p>
</div>
<AppList
apps={results}
labelledBy="developed-by"
matchAgainst={selectedMatch}
to={getAppPath}
/>
{status === 'finished' ? (
<p>That&apos;s it!</p>
) : (
<p>Awaiting {awaiting} more</p>
)}
</>
) : (
<div id="developed-by"> <div id="developed-by">
<h2 className="mb-3"> <h2 className="mb-3">
Software developed by <ShipName name={provider} className="font-mono" /> Software developed by{' '}
<ShipName name={provider} className="font-mono" />
</h2> </h2>
<p> <p>No apps found</p>
{count} result{count === 1 ? '' : 's'}
</p>
</div> </div>
<AppList ))}
apps={results} {status === 'error' && (
labelledBy="developed-by"
matchAgainst={selectedMatch}
to={getAppPath}
/>
<p>That&apos;s it!</p>
</>
)}
{showNone && (
<h2> <h2>
Unable to find software developed by <ShipName name={provider} className="font-mono" /> Unable to connect to{' '}
<ShipName name={provider} className="font-mono" />
</h2> </h2>
)} )}
</div> </div>

View File

@ -25,6 +25,7 @@ import {
import api from '@/api'; import api from '@/api';
import { normalizeUrbitColor } from '@/logic/utils'; import { normalizeUrbitColor } from '@/logic/utils';
import { Status } from '@/logic/useAsyncCall'; import { Status } from '@/logic/useAsyncCall';
import { ConnectionStatus, useConnectivityCheck } from './vitals';
export interface ChargeWithDesk extends Charge { export interface ChargeWithDesk extends Charge {
desk: string; desk: string;
@ -261,42 +262,43 @@ export function useAllies() {
return useDocketState(selAllies); return useDocketState(selAllies);
} }
function getAllyTreatyStatus(
treaties: Treaties,
fetching: boolean,
alliance?: string[],
status?: ConnectionStatus
): Status | 'awaiting' | 'partial' | 'finished' {
const treatyCount = Object.keys(treaties).length;
console.log(treatyCount, alliance?.length, fetching, status);
if (alliance && alliance.length !== 0 && treatyCount === alliance.length) {
return 'finished';
}
if (treatyCount > 0) {
return 'partial';
}
if (!status || ('complete' in status && status.complete === 'no-data')) {
return 'initial';
}
if (fetching || 'pending' in status) {
return 'loading';
}
if ('complete' in status && status.complete === 'yes') {
return alliance && alliance.length > 0 ? 'awaiting' : 'finished';
}
return 'error';
}
export function useAllyTreaties(ship: string) { export function useAllyTreaties(ship: string) {
const { data } = useConnectivityCheck(ship);
const allies = useAllies(); const allies = useAllies();
const isAllied = ship in allies; const isAllied = ship in allies;
const [status, setStatus] = useState<Status>('initial'); const [fetching, setFetching] = useState(false);
const [treaties, setTreaties] = useState<Treaties>(); const treaties = useDocketState(
useEffect(() => {
if (Object.keys(allies).length > 0 && !isAllied) {
setStatus('loading');
useDocketState.getState().addAlly(ship);
}
}, [allies, isAllied, ship]);
useEffect(() => {
async function fetchTreaties() {
if (isAllied) {
setStatus('loading');
try {
const newTreaties = await useDocketState
.getState()
.fetchAllyTreaties(ship);
if (Object.keys(newTreaties).length > 0) {
setTreaties(newTreaties);
setStatus('success');
}
} catch {
setStatus('error');
}
}
}
fetchTreaties();
}, [ship, isAllied]);
const storeTreaties = useDocketState(
useCallback( useCallback(
(s) => { (s) => {
const charter = s.allies[ship]; const charter = s.allies[ship];
@ -305,27 +307,42 @@ export function useAllyTreaties(ship: string) {
[ship] [ship]
) )
); );
debugger;
const status = getAllyTreatyStatus(
treaties,
fetching,
allies[ship],
data?.status
);
useEffect(() => { useEffect(() => {
const timeout = setTimeout(() => { if (Object.keys(allies).length > 0 && !isAllied) {
setStatus('error'); useDocketState.getState().addAlly(ship);
}, 30 * 1000); // wait 30 secs before timing out }
}, [allies, isAllied, ship]);
if (Object.keys(storeTreaties).length > 0) { useEffect(() => {
setTreaties(storeTreaties); async function fetchTreaties() {
setStatus('success'); try {
clearTimeout(timeout); setFetching(true);
await useDocketState.getState().fetchAllyTreaties(ship);
setFetching(false);
} catch {
console.log("couldn't fetch initial treaties");
}
} }
return () => { if (isAllied) {
clearTimeout(timeout); fetchTreaties();
}; }
}, [storeTreaties]); }, [ship, isAllied]);
return { return {
isAllied, isAllied,
treaties, treaties,
status, status,
connection: data,
awaiting: allies[ship]?.length || 0 - Object.keys(treaties).length,
}; };
} }

153
ui/src/state/vitals.ts Normal file
View File

@ -0,0 +1,153 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import api from '@/api';
import { useEffect, useState } from 'react';
interface Connected {
complete: 'yes';
}
interface YetToCheck {
complete: 'no-data';
}
interface NoDNS {
complete: 'no-dns';
}
interface Crash {
complete: 'crash';
crash: string[][];
}
interface NoOurPlanet {
complete: 'no-our-planet';
'last-contact': number;
}
interface NoOurGalaxy {
complete: 'no-our-galaxy';
'last-contact': number;
}
interface NoSponsorHit {
complete: 'no-sponsor-hit';
ship: string;
}
interface NoSponsorMiss {
complete: 'no-sponsor-miss';
ship: string;
}
interface NoTheirGalaxy {
complete: 'no-their-galaxy';
'last-contact': number;
}
export type ConnectionCompleteStatusKey =
| 'yes'
| 'crash'
| 'no-data'
| 'no-dns'
| 'no-our-planet'
| 'no-our-galaxy'
| 'no-sponsor-hit'
| 'no-sponsor-miss'
| 'no-their-galaxy';
export interface CompleteStatus {
complete: ConnectionCompleteStatusKey;
}
export type ConnectionCompleteStatus =
| Connected
| YetToCheck
| Crash
| NoDNS
| NoOurPlanet
| NoOurGalaxy
| NoSponsorHit
| NoSponsorMiss
| NoTheirGalaxy;
export type ConnectionPendingStatusKey =
| 'setting-up'
| 'trying-dns'
| 'trying-local'
| 'trying-target'
| 'trying-sponsor';
export type ConnectionPendingStatus =
| {
pending: Omit<ConnectionPendingStatusKey, 'trying-sponsor'>;
}
| {
pending: 'trying-sponsor';
ship: string;
};
export type ConnectionStatus =
| ConnectionCompleteStatus
| ConnectionPendingStatus;
export interface ConnectionUpdate {
status: ConnectionStatus;
timestamp: number;
}
export function useConnectivityCheck(ship: string, useStale = false) {
const [subbed, setSubbed] = useState(false);
const queryClient = useQueryClient();
const query = useQuery(
['vitals', ship],
async (): Promise<ConnectionUpdate> => {
const resp = await api.scry<ConnectionUpdate>({
app: 'vitals',
path: `/ship/${ship}`,
});
const now = Date.now();
const diff = now - resp.timestamp;
// if status older than 30 seconds, re-run check
if (diff > 30 * 1000 || !useStale) {
api.poke({
app: 'vitals',
mark: 'run-check',
json: ship,
});
return {
status: {
pending: 'setting-up',
},
timestamp: now,
};
}
return resp;
},
{
enabled: subbed,
cacheTime: 0,
initialData: {
status: {
pending: 'setting-up',
},
timestamp: Date.now(),
},
}
);
useEffect(() => {
api.subscribe({
app: 'vitals',
path: `/status/${ship}`,
event: (data: ConnectionUpdate) => {
queryClient.setQueryData(['vitals', ship], data);
},
});
setSubbed(true);
}, [ship]);
return query;
}