Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

settings updates #148

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
9 changes: 7 additions & 2 deletions ui/src/logic/useAsyncCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ import { useCallback, useState } from 'react';

export type Status = 'initial' | 'loading' | 'success' | 'error';

export function useAsyncCall<ReturnValue>(cb: (...args: any[]) => Promise<ReturnValue>) {
export function useAsyncCall<ReturnValue>(
cb: (...args: any[]) => Promise<ReturnValue>
) {
const [status, setStatus] = useState<Status>('initial');
const [result, setResult] = useState<ReturnValue | null>(null);
const [error, setError] = useState<Error | null>(null);

const call = useCallback(
(...args: any[]) => {
setStatus('loading');
cb(...args)
.then((result) => {
setResult(result);
setStatus('success');
return result;
})
Expand All @@ -25,6 +29,7 @@ export function useAsyncCall<ReturnValue>(cb: (...args: any[]) => Promise<Return
return {
call,
status,
error
error,
result,
};
}
35 changes: 30 additions & 5 deletions ui/src/preferences/SystemPreferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import { Bullet } from '../components/icons/Bullet';
import SearchSystemPreferences from './SearchSystemPrefences';
import { ShortcutPrefs } from './ShortcutPrefs';
import { AttentionAndPrivacy } from './AttentionAndPrivacy';
import { Avatar } from '../components/Avatar';
import { SystemResourcePrefs } from './system-resources/SystemResourcePrefs';

interface SystemPreferencesSectionProps {
url: string;
Expand All @@ -49,7 +51,7 @@ function SystemPreferencesSection({
url,
active,
children,
visible=true,
visible = true,
}: PropsWithChildren<SystemPreferencesSectionProps>) {
return (
<li>
Expand All @@ -76,8 +78,9 @@ export const SystemPreferences = (
);
const { systemBlocked } = useSystemUpdate();
const charges = useCharges();
const filteredCharges = Object.values(charges)
.filter((charge) => charge.desk !== 'landscape');
const filteredCharges = Object.values(charges).filter(
(charge) => charge.desk !== 'landscape'
);
const isMobile = useMedia('(max-width: 639px)');
const settingsPath = isMobile ? `${match.url}/:submenu` : '/';

Expand Down Expand Up @@ -116,6 +119,24 @@ export const SystemPreferences = (
<nav className="flex flex-col px-2 sm:px-6">
<SearchSystemPreferences subUrl={subUrl} />
<span className="pt-1 pl-2 pb-3 text-sm font-semibold text-gray-400">
System
</span>
<ul className="space-y-1">
<SystemPreferencesSection
url={subUrl('system')}
active={matchSub('system')}
>
<Avatar shipName={window.ship} size="xs" className="mr-3" />
<span>~{window.ship}</span>
{systemBlocked && (
<Bullet className="ml-auto h-5 w-5 text-orange-500" />
)}
</SystemPreferencesSection>
</ul>
</nav>

<nav className="flex flex-col px-2 sm:px-6">
<span className="pt-5 pl-2 pb-3 text-sm font-semibold text-gray-400">
Landscape
</span>
<ul className="space-y-1">
Expand All @@ -124,7 +145,7 @@ export const SystemPreferences = (
active={matchSub('system-updates')}
>
<TlonIcon className="mr-3 h-6 w-6 rounded-md text-gray-600" />
About System
About
{systemBlocked && (
<Bullet className="ml-auto h-5 w-5 text-orange-500" />
)}
Expand Down Expand Up @@ -217,7 +238,7 @@ export const SystemPreferences = (
<DocketImage size="small" className="mr-3" {...charge} />
{getAppName(charge)}
</SystemPreferencesSection>
))}
))}
</ul>
</nav>
</aside>
Expand All @@ -227,6 +248,10 @@ export const SystemPreferences = (
<Switch>
<Route path={`${match.url}/apps/:desk`} component={AppPrefs} />
<Route path={`${match.url}/help`} component={Help} />
<Route
path={`${match.url}/system`}
component={SystemResourcePrefs}
/>
<Route
path={`${match.url}/interface`}
component={InterfacePrefs}
Expand Down
102 changes: 102 additions & 0 deletions ui/src/preferences/system-resources/IdentityPrefs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import classNames from 'classnames';
import React, {
ComponentProps,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { Avatar } from '../../components/Avatar';
import { Button } from '../../components/Button';
import { useAzimuthState } from '../../state/azimuth';
import clipboardCopy from 'clipboard-copy';

export const IdentityPrefs = () => {
const { state, stateLoadStatus, forceUpdate } = useAzimuthState();

return (
<div className="inner-section space-y-8">
<h2 className="h4">Identity</h2>
<div className="flex flex-row items-center">
<Avatar shipName={window.ship} size="default" />
<div className="ml-2 flex-grow">
<h3 className="mb-1 font-semibold">~{window.ship}</h3>
{stateLoadStatus === 'loading' && (
<div className="flex flex-row items-center justify-start">
<span
className={classNames('font-semibold', {
'text-orange-400': state?.stale,
'text-gray-400': !state?.stale,
})}
>
Azimuth block: {state?.block}
</span>
{state?.stale && (
<span className="ml-1 rounded border border-solid border-orange-400 px-1.5 text-xs font-semibold uppercase text-orange-400">
Stale
</span>
)}
</div>
)}
</div>
<div>
{state?.block && (
<CopyButton
type="submit"
variant="secondary"
className="py-1 px-3 text-sm"
label="Copy Info"
content={state.block}
></CopyButton>
)}
{stateLoadStatus === 'success' && state?.stale && (
<Button
type="submit"
className="ml-1 bg-orange-400 py-1 px-3 text-sm"
onClick={forceUpdate}
>
Update
</Button>
)}
</div>
</div>
</div>
);
};

type CopyButtonProps = Omit<
ComponentProps<typeof Button>,
'children' | 'onClick'
>;

const CopyButton = ({
label = 'copy',
content,
...buttonProps
}: {
label: string;
content: string;
} & CopyButtonProps) => {
const [successMessageActive, setSuccessMesageActive] = useState(false);
const copyTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);

const copy = useCallback(() => {
if (copyTimeout.current) clearTimeout(copyTimeout.current);
clipboardCopy(content);
setSuccessMesageActive(true);
copyTimeout.current = setTimeout(() => setSuccessMesageActive(false), 1000);
}, []);

// ensure timeout is cleared when component unmounts
useEffect(() => {
() => {
if (copyTimeout.current) clearTimeout(copyTimeout.current);
};
}, []);

return (
<Button {...buttonProps} onClick={copy}>
{successMessageActive ? 'Copied' : label}
</Button>
);
};
41 changes: 41 additions & 0 deletions ui/src/preferences/system-resources/LoomPrefs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { Button } from '../../components/Button';
import { useLoom } from '../../state/loom';

export const LoomPrefs = () => {
const { free, total, pack } = useLoom();

return (
<div className="inner-section mt-4 space-y-8 leading-5">
<h2 className="h4">Loom</h2>
<div className="space-y-3">
<div className="overflow-hidden rounded-full rounded bg-green-200 ">
<div
className="h-2 bg-green-300"
style={{ width: (free / total) * 100 + '%' }}
></div>
</div>
<label className="flex flex-row items-center justify-between text-sm font-semibold">
{Math.round(free / 1000)}/{Math.round(total / 1000)}mb free
</label>
</div>

<div>
<h3 className="font-semibold">Optimize Your Urbit's Loom</h3>
<p className="text-gray-400">
Deduplicate objects in storage to free up space using |pack.{' '}
<a
href="https://operators.urbit.org/manual/running/vere#pack"
target="_blank"
className="underline"
>
Learn more
</a>
</p>
<Button variant="primary" className="mt-4" onClick={pack}>
Pack Loom
</Button>
</div>
</div>
);
};
96 changes: 96 additions & 0 deletions ui/src/preferences/system-resources/P2PServicePrefs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import classNames from 'classnames';
import React from 'react';
import { Avatar } from '../../components/Avatar';
import { Button } from '../../components/Button';
import { Spinner } from '../../components/Spinner';
import { Bullet } from '../../components/icons/Bullet';
import { Cross } from '../../components/icons/Cross';
import {
AvailabilityStatus,
usePeerDiscoveryShips,
useShipAvailability,
} from '../../state/connectivity';

export const P2PServicePrefs = () => {
const { peerDiscoveryShips } = usePeerDiscoveryShips();

return (
<div className="inner-section mt-4 space-y-8 leading-5">
<h2 className="h4">P2P Services</h2>
<div className="space-y-3">
<div>
<h3 className="font-semibold">Peer Discovery</h3>
<p className="text-gray-400">Ships your Urbit uses to find peers</p>
</div>
<ul className="mt-4 flex flex-col space-y-2">
{peerDiscoveryShips?.map((ship) => (
<li>
<ConnectivityTester shipName={ship} />
</li>
))}
</ul>
</div>
</div>
);
};

const ConnectivityTester = ({ shipName }: { shipName: string }) => {
const { status, availability, checkAvailability } =
useShipAvailability(shipName);

return (
<div className="flex w-full flex-row items-center">
<div className="flex flex-1 flex-row items-center space-x-2">
<Avatar size="small" shipName={shipName}></Avatar>
<h3 className="font-semibold">{shipName}</h3>
<AvailabilityIndicator
status={availability?.status || 'initial'}
isChecking={status === 'loading'}
className="flex-1"
/>
<Button
type="submit"
className="py-1 px-3 text-sm"
variant="secondary"
disabled={status === 'loading'}
onClick={checkAvailability}
>
Test Connection
</Button>
</div>
</div>
);
};

const AvailabilityIndicator = ({
isChecking,
status,
className,
}: {
isChecking: boolean;
status: AvailabilityStatus;
className?: string;
}) => {
return (
<div className={classNames('flex space-x-0.5', className)}>
{isChecking ? (
<Spinner className="h-4 w-4 text-blue-500" />
) : (
<div className="flex flex-1 flex-row items-center">
{status === 'available' && (
<>
<span className="font-semibold text-blue-400">Available</span>
<Bullet className="h-5 w-5 text-blue-500" />
</>
)}
{status === 'unavailable' && (
<>
<span className="font-semibold text-orange-400">Unavailable</span>
<Cross className="h-5 w-5 p-1 text-orange-400" />
</>
)}
</div>
)}
</div>
);
};
12 changes: 12 additions & 0 deletions ui/src/preferences/system-resources/SystemResourcePrefs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import { IdentityPrefs } from './IdentityPrefs';
import { P2PServicePrefs } from './P2PServicePrefs';
import { LoomPrefs } from './LoomPrefs';

export const SystemResourcePrefs = () => (
<>
<IdentityPrefs />
<P2PServicePrefs />
<LoomPrefs />
</>
);
Loading