Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Handle custom httpd port
Sebastian Martinez committed 2 years ago
commit 52cece4680ccaa295a32bfcbd5eb9dbe6cca6284
parent 4fcb7373a331c62754b1658ed5b88658ac967f2e
11 files changed +186 -36
modified httpd-client/index.ts
@@ -115,6 +115,28 @@ export class HttpdClient {
    this.session = new session.Client(this.#fetcher);
  }

+
  public changePort(port: number): void {
+
    this.#baseUrl.port = port;
+
  }
+

+
  public get url(): string {
+
    return `${this.#baseUrl.scheme}://${this.#baseUrl.hostname}:${
+
      this.#baseUrl.port
+
    }`;
+
  }
+

+
  public get hostnamePort(): string {
+
    return `${this.#baseUrl.hostname}:${this.#baseUrl.port}`;
+
  }
+

+
  public get hostname(): string {
+
    return this.#baseUrl.hostname;
+
  }
+

+
  public get port(): string {
+
    return this.#baseUrl.port.toString();
+
  }
+

  public async getNodeInfo(options?: RequestOptions): Promise<NodeInfo> {
    return this.#fetcher.fetchOk(
      {
modified src/App/Header/Connect.svelte
@@ -1,8 +1,9 @@
<script lang="ts">
  import type { HttpdState } from "@app/lib/httpd";

+
  import * as httpd from "@app/lib/httpd";
  import { closeFocused } from "@app/components/Floating.svelte";
-
  import { httpdStore, disconnect } from "@app/lib/httpd";
+
  import { httpdStore } from "@app/lib/httpd";

  import Authorship from "@app/components/Authorship.svelte";
  import Button from "@app/components/Button.svelte";
@@ -11,11 +12,15 @@
  import Floating from "@app/components/Floating.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Link from "@app/components/Link.svelte";
+
  import PortInput from "@app/App/Header/Connect/PortInput.svelte";

  $: command = import.meta.env.PROD
-
    ? "rad web"
-
    : `rad web --frontend ${new URL(import.meta.url).origin}`;
+
    ? `rad web --backend ${httpd.api.url}`
+
    : `rad web --frontend ${new URL(import.meta.url).origin} --backend ${
+
        httpd.api.url
+
      }`;

+
  let customPort = httpd.api.port;
  const buttonTitle: Record<HttpdState["state"], string> = {
    stopped: "radicle-httpd is stopped",
    running: "radicle-httpd is running",
@@ -56,7 +61,7 @@
    height: 2.5rem;
    justify-content: space-between;
    line-height: 2.5rem;
-
    padding: 0 0.8rem;
+
    padding: 0 1rem;
    user-select: none;
    width: 100%;
  }
@@ -64,7 +69,7 @@
    background-color: var(--color-foreground-3);
    color: var(--color-foreground-6);
  }
-
  .rounded {
+
  .rounded:last-of-type:hover {
    border-bottom-left-radius: var(--border-radius);
    border-bottom-right-radius: var(--border-radius);
  }
@@ -114,7 +119,10 @@
          on:afterNavigate={closeFocused}
          route={{
            resource: "seeds",
-
            params: { hostnamePort: "radicle.local", projectPageIndex: 0 },
+
            params: {
+
              hostnamePort: httpd.api.hostnamePort,
+
              projectPageIndex: 0,
+
            },
          }}>
          <div class="dropdown-button">Browse</div>
        </Link>
@@ -123,7 +131,7 @@
        <div
          class="dropdown-button rounded"
          on:click={() => {
-
            void disconnect();
+
            void httpd.disconnect();
            closeFocused();
          }}>
          Disconnect
@@ -135,14 +143,18 @@
          To connect to your local Radicle node, run this command in your
          terminal:
        </div>
-
        <div style:margin="0 1rem 1rem 1rem">
+
        <div style:margin="0 1rem 0.5rem 1rem">
          <Command {command} />
        </div>
+
        <PortInput bind:port={customPort} />
        <Link
          on:afterNavigate={closeFocused}
          route={{
            resource: "seeds",
-
            params: { hostnamePort: "radicle.local", projectPageIndex: 0 },
+
            params: {
+
              hostnamePort: httpd.api.hostnamePort,
+
              projectPageIndex: 0,
+
            },
          }}>
          <div class="dropdown-button rounded">Browse</div>
        </Link>
@@ -155,6 +167,7 @@
        <div style:margin="0.5rem 1rem 1rem 1rem">
          <Command command="radicle-httpd" />
        </div>
+
        <PortInput bind:port={customPort} />
      </div>
    {/if}
  </div>
added src/App/Header/Connect/PortInput.svelte
@@ -0,0 +1,34 @@
+
<script lang="ts">
+
  import * as httpd from "@app/lib/httpd";
+

+
  import TextInput from "@app/components/TextInput.svelte";
+

+
  export let port: string;
+

+
  $: validPortNumber = Number(port) > 0 && Number(port) <= 65535;
+
</script>
+

+
<style>
+
  .item {
+
    display: flex;
+
    flex-direction: row;
+
    align-items: center;
+
    padding: 0.5rem 1rem 0.5rem 1rem;
+
    justify-content: space-between;
+
    border-top: 1px solid var(--color-foreground-3);
+
    height: 2.5rem;
+
  }
+
</style>
+

+
<div class="item">
+
  <span>Port</span>
+
  <div style:width="6.5rem">
+
    <TextInput
+
      name="port"
+
      size="small"
+
      variant="modal"
+
      bind:value={port}
+
      valid={validPortNumber}
+
      on:submit={() => httpd.changeHttpdPort(Number(port))} />
+
  </div>
+
</div>
modified src/App/Header/Search.svelte
@@ -123,13 +123,9 @@
      {loading}
      disabled={loading}
      bind:value={input}
-
      on:focus={() => {
-
        expanded = true;
-
      }}
+
      on:focus={() => (expanded = true)}
      on:blur={() => {
-
        if (input === "") {
-
          expanded = false;
-
        }
+
        if (input === "") expanded = false;
      }}
      on:submit={search}
      placeholder={searchPlaceholder}>
modified src/components/TextInput.svelte
@@ -1,14 +1,17 @@
<script lang="ts" strictEvents>
+
  import debounce from "lodash/debounce";
  import { createEventDispatcher } from "svelte";
  import { onMount } from "svelte";

+
  import Icon from "@app/components/Icon.svelte";
  import Loading from "@app/components/Loading.svelte";

  export let name: string | undefined = undefined;
  export let placeholder: string | undefined = undefined;
  export let value: string | undefined = undefined;

-
  export let variant: "regular" | "form" = "regular";
+
  export let variant: "regular" | "form" | "modal" = "regular";
+
  export let size: "regular" | "small" = "regular";

  export let autofocus: boolean = false;
  export let disabled: boolean = false;
@@ -17,12 +20,16 @@
  export let validationMessage: string | undefined = undefined;

  const dispatch = createEventDispatcher<{
+
    blur: FocusEvent;
+
    focus: FocusEvent;
    submit: never;
  }>();

  let rightContainerWidth: number;
  let leftContainerWidth: number;
  let inputElement: HTMLInputElement | undefined = undefined;
+
  let isFocused = false;
+
  let success = false;

  onMount(() => {
    if (autofocus && inputElement) {
@@ -31,15 +38,30 @@
    }
  });

+
  const restoreIcon = debounce(() => {
+
    success = false;
+
  }, 800);
+

  function handleKeydown(event: KeyboardEvent) {
    if (event.key === "Enter" && valid) {
+
      success = true;
      dispatch("submit");
+
      restoreIcon();
    }

    if (event.key === "Escape") {
      inputElement?.blur();
    }
  }
+

+
  function handleFocusEvent(e: FocusEvent) {
+
    if (isFocused) {
+
      dispatch("blur", e);
+
    } else {
+
      dispatch("focus", e);
+
    }
+
    isFocused = !isFocused;
+
  }
</script>

<style>
@@ -49,7 +71,7 @@
    margin: 0;
    position: relative;
    flex: 1;
-
    height: 2.5rem;
+
    height: var(--button-regular-height);
  }
  input {
    background: transparent;
@@ -76,18 +98,25 @@
    border: 1px solid var(--color-secondary);
    padding: 1rem 1.5rem;
  }
-
  .form {
+
  .form,
+
  .modal {
    background: var(--color-foreground-1);
    border-radius: var(--border-radius-small);
    border: 1px solid var(--color-foreground-1);
  }
-
  .form::placeholder {
+
  .form::placeholder,
+
  .modal::placeholder {
    color: var(--color-foreground-5);
  }
  .form:focus,
-
  .form:hover {
+
  .form:hover,
+
  .modal:focus,
+
  .modal:hover {
    border: 1px solid var(--color-foreground-4);
  }
+
  .modal {
+
    background: var(--color-background);
+
  }
  .left-container {
    color: var(--color-secondary);
    position: absolute;
@@ -112,6 +141,10 @@
    padding-left: 0.5rem;
    gap: 0.5rem;
  }
+
  .small {
+
    height: var(--button-small-height);
+
    font-size: var(--font-size-small);
+
  }
  .validation-message {
    color: var(--color-negative);
    font-size: var(--font-size-small);
@@ -134,7 +167,7 @@
  }
</style>

-
<div class="wrapper">
+
<div class="wrapper" class:small={size === "small"}>
  <div class="validation-wrapper">
    <div class="left-container" bind:clientWidth={leftContainerWidth}>
      {#if $$slots.left}
@@ -145,6 +178,8 @@
    <input
      class:regular={variant === "regular"}
      class:form={variant === "form"}
+
      class:modal={variant === "modal"}
+
      class:small={size === "small"}
      style:padding-left={leftContainerWidth
        ? `${leftContainerWidth}px`
        : "auto"}
@@ -158,13 +193,17 @@
      {disabled}
      bind:value
      on:input
-
      on:focus
-
      on:blur
+
      on:focus={handleFocusEvent}
+
      on:blur={handleFocusEvent}
      on:keydown|stopPropagation={handleKeydown}
      on:click
      on:change />

-
    <div class="right-container" bind:clientWidth={rightContainerWidth}>
+
    <div
+
      class="right-container"
+
      class:small={size === "small"}
+
      style:padding-right={variant === "modal" ? "0.5rem" : "1rem"}
+
      bind:clientWidth={rightContainerWidth}>
      {#if $$slots.right}
        <slot name="right" />
      {/if}
@@ -173,8 +212,12 @@
        <Loading small noDelay />
      {/if}

-
      {#if valid && !loading}
-
        <div class="key-hint">⏎</div>
+
      {#if valid && !loading && isFocused}
+
        {#if success}
+
          <Icon name="checkmark" size="small" />
+
        {:else}
+
          <div class="key-hint">⏎</div>
+
        {/if}
      {/if}
    </div>

modified src/lib/httpd.ts
@@ -17,11 +17,12 @@ export type HttpdState =
    };

const HTTPD_STATE_STORAGE_KEY = "httpdState";
+
const HTTPD_CUSTOM_PORT_KEY = "httpdCustomPort";

const store = writable<HttpdState>({ state: "stopped" });
export const httpdStore = derived(store, s => s);

-
const api = new HttpdClient({
+
export const api = new HttpdClient({
  hostname: "127.0.0.1",
  port: 8080,
  scheme: "http",
@@ -29,6 +30,11 @@ const api = new HttpdClient({

let pollHttpdStateHandle: number | undefined = undefined;

+
export function changeHttpdPort(port: number) {
+
  window.localStorage.setItem(HTTPD_CUSTOM_PORT_KEY, String(port));
+
  void checkState();
+
}
+

function update(state: HttpdState) {
  window.localStorage.setItem(HTTPD_STATE_STORAGE_KEY, JSON.stringify(state));
  store.set(state);
@@ -89,6 +95,10 @@ export async function disconnect() {
async function checkState() {
  let httpdState: HttpdState | null = null;
  const rawHttpdState = window.localStorage.getItem(HTTPD_STATE_STORAGE_KEY);
+
  const customHttpdPort = window.localStorage.getItem(HTTPD_CUSTOM_PORT_KEY);
+
  if (customHttpdPort) {
+
    api.changePort(Number(customHttpdPort));
+
  }
  if (rawHttpdState) {
    try {
      httpdState = JSON.parse(rawHttpdState);
@@ -140,8 +150,9 @@ export function initialize() {
  // Sync session state changes with other open tabs and windows.
  addEventListener("storage", event => {
    if (
-
      event.key === HTTPD_STATE_STORAGE_KEY &&
-
      event.oldValue !== event.newValue
+
      (event.key === HTTPD_STATE_STORAGE_KEY &&
+
        event.oldValue !== event.newValue) ||
+
      (event.key === HTTPD_CUSTOM_PORT_KEY && event.oldValue !== event.newValue)
    ) {
      void checkState();
    }
modified src/lib/utils.ts
@@ -255,10 +255,11 @@ export function extractBaseUrl(hostnamePort: string): BaseUrl {
  ) {
    return { hostname: "127.0.0.1", port: 8080, scheme: "http" };
  } else if (hostnamePort.includes(":")) {
+
    const [hostname, port] = hostnamePort.split(":");
    return {
-
      hostname: hostnamePort.split(":")[0],
-
      port: Number(hostnamePort.split(":")[1]),
-
      scheme: config.seeds.defaultHttpdScheme,
+
      hostname,
+
      port: Number(port),
+
      scheme: isLocal(hostname) ? "http" : config.seeds.defaultHttpdScheme,
    };
  } else {
    return {
modified src/views/projects/View.svelte
@@ -4,6 +4,7 @@
  import type { Project } from "@httpd-client";
  import type { ProjectLoadedView } from "@app/views/projects/router";

+
  import * as httpd from "@app/lib/httpd";
  import * as router from "@app/lib/router";
  import * as utils from "@app/lib/utils";
  import { HttpdClient } from "@httpd-client";
@@ -49,8 +50,8 @@
    void router.push({
      resource: "projects",
      params: {
-
        id: id,
-
        hostnamePort: baseUrl.hostname,
+
        id,
+
        hostnamePort: httpd.api.hostnamePort,
        view: {
          resource: "issue",
          params: { issue: issueId },
modified src/views/session/Index.svelte
@@ -20,7 +20,7 @@
      modal.show({ component: AuthenticatedModal, props: {} });
      void router.push({
        resource: "seeds",
-
        params: { hostnamePort: "radicle.local", projectPageIndex: 0 },
+
        params: { hostnamePort: httpd.api.hostnamePort, projectPageIndex: 0 },
      });
    } else {
      modal.show({
modified tests/e2e/hotkeys.spec.ts
@@ -29,7 +29,7 @@ test("global hotkeys", async ({ page }) => {
    await expect(page.getByPlaceholder(searchPlaceholder)).toHaveValue(
      "searchquery?",
    );
-
    await expect(page.getByText("⏎")).toBeVisible();
+
    await expect(page.getByText("⏎")).toBeHidden();
    await expect(page.getByPlaceholder(searchPlaceholder)).not.toBeFocused();
  }
});
added tests/e2e/httpd.spec.ts
@@ -0,0 +1,29 @@
+
import { test, expect } from "@tests/support/fixtures.js";
+

+
test("change httpd port", async ({ page, peerManager }) => {
+
  const peer = await peerManager.startPeer({ name: "httpd" });
+

+
  await peer.startHttpd(8070);
+
  await peer.startNode();
+

+
  await page.goto("/");
+
  await page.getByRole("button", { name: "radicle.local" }).click();
+

+
  await page.locator('input[name="port"]').fill("8070");
+
  await page.locator('input[name="port"]').press("Enter");
+

+
  const { stdout } = await peer.rad([
+
    "web",
+
    "--frontend",
+
    "http://localhost:3000",
+
    "--backend",
+
    "http://127.0.0.1:8070",
+
  ]);
+
  const match = stdout.trim().match(/(http:\/\/localhost:3000\/.*)$/);
+
  if (!match) {
+
    throw Error("Not able to parse auth url");
+
  }
+
  await page.goto(match[0]);
+
  await expect(page.getByText("Authenticated")).toBeVisible();
+
  await expect(page).toHaveURL("/seeds/127.0.0.1:8070");
+
});