~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
142 lines · 4.6 KB Raw History
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { Notice } from '#/components/atoms/notice.jsx';
import { Panel } from '#/components/atoms/panel.jsx';
import { ErrorNotice } from '#/components/molecules/error-notice.jsx';
import { Section } from '#/components/molecules/section.jsx';
import { SelectField } from '#/components/molecules/select-field.jsx';
import { TextLink } from '#/components/molecules/text-link.jsx';
import { get, post } from '#/lib/api.js';
import { queryKeys, useApiMutation } from '#/lib/query.js';

/**
 * The update page for this deployment, with this server's hostname, worker
 * script name and release channel carried along so the page can find the
 * worker and install from the channel this server follows.
 * @param {string} updateUrl
 * @param {{scriptName?: string|null, channel?: string}} [version]
 */
export function updateHref(updateUrl, version = {}) {
  const url = new URL(updateUrl);
  url.searchParams.set('host', window.location.host);
  if (version.scriptName) url.searchParams.set('script', version.scriptName);
  if (version.channel === 'canary') url.searchParams.set('channel', 'canary');
  return url.toString();
}

const CHANNELS = [
  ['stable', 'Stable releases'],
  ['canary', 'Canary builds'],
];

/**
 * The pds.js release this server runs, whether a newer one is published, and
 * which release channel the check follows. The server does the lookup and
 * caches it, so rendering this card costs one same-origin request.
 * Installing a release is a redeploy of the server: with an update page
 * configured the card links there, and without one it names the release and
 * leaves the redeploy to the operator.
 * @param {{data: {features: {settings: boolean}}}} props
 */
export function VersionCard({ data: overview }) {
  const { data } = useQuery({
    queryKey: queryKeys.version,
    queryFn: () => get('/version'),
  });
  const canStore = overview.features.settings;
  const { run, busy, error } = useApiMutation([queryKeys.version]);
  const [pending, setPending] = useState(null);
  const channel = pending ?? data?.channel ?? 'stable';

  const changeChannel = (next) => {
    const previous = channel;
    setPending(next);
    run(async () => {
      try {
        await post('/settings/release-channel', { channel: next });
      } catch (err) {
        setPending(previous);
        throw err;
      }
    });
  };

  let status;
  if (!data) {
    status = (
      <p className="text-[13px] text-muted-foreground">
        Checking for the latest release.
      </p>
    );
  } else if (data.updateAvailable) {
    status = (
      <Notice variant="info">
        <span className="font-bold text-foreground">
          Version {data.latest} is available.
        </span>{' '}
        {data.updateUrl ? (
          <>
            <TextLink href={updateHref(data.updateUrl, data)} external>
              Update this server
            </TextLink>{' '}
            to install it.{' '}
          </>
        ) : (
          <>Update the server deployment to install it. </>
        )}
        <TextLink href={data.releaseNotesUrl} external>
          Release notes
        </TextLink>
      </Notice>
    );
  } else if (data.latest) {
    status = (
      <p className="text-[13px] text-muted-foreground">
        {data.channel === 'canary'
          ? 'This is the newest canary build.'
          : 'This is the latest release.'}
      </p>
    );
  } else {
    status = (
      <p className="text-[13px] text-muted-foreground">
        The release registry could not be reached. Checking again later.
      </p>
    );
  }

  return (
    <Panel>
      <Section
        title="Server software"
        description="The pds.js release this server runs."
      >
        <div className="flex flex-col gap-4">
          <p className="font-bold text-[15px] text-foreground">
            {data ? `Version ${data.current}` : ' '}
          </p>
          {status}
          <SelectField
            label="Release channel"
            options={CHANNELS}
            value={channel}
            onChange={changeChannel}
            disabled={busy || !data || !canStore}
          />
          {channel === 'canary' && (
            <p className="text-[13px] text-muted-foreground">
              Canary follows the newest build of the main branch. It changes
              often and gets less testing than a release.
            </p>
          )}
          {!canStore && (
            <p className="text-[13px] text-muted-foreground">
              This server does not store settings.
            </p>
          )}
          <ErrorNotice error={error} />
        </div>
      </Section>
    </Panel>
  );
}