mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-27 15:40:36 +08:00
Handle custom httpd port
We only supported fetching the default httpd port (8080). This commit removes navigation to `radicle.local` which represents `0.0.0.0:8080`, and prepares the app for defining custom port. Signed-off-by: Sebastian Martinez <me@sebastinez.dev>
This commit is contained in:
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
+14
-3
@@ -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();
|
||||
}
|
||||
|
||||
+4
-3
@@ -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 {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
Reference in New Issue
Block a user