mirror of
https://github.com/tale/headplane.git
synced 2026-09-02 18:28:13 +00:00
feat: update more primitives to use the new api
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { CheckCircle, CircleSlash, Info, UserCircle } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { data, Link as RemixLink, useLoaderData } from 'react-router';
|
||||
import { data, Link as RemixLink } from 'react-router';
|
||||
import Attribute from '~/components/Attribute';
|
||||
import Button from '~/components/Button';
|
||||
import Card from '~/components/Card';
|
||||
@@ -58,9 +58,9 @@ export async function loader({ request, params, context }: Route.LoaderArgs) {
|
||||
|
||||
export const action = machineAction;
|
||||
|
||||
export default function Page() {
|
||||
const { node, tags, magic, users, agent, stats } =
|
||||
useLoaderData<typeof loader>();
|
||||
export default function Page({
|
||||
loaderData: { node, tags, users, magic, agent, stats },
|
||||
}: Route.ComponentProps) {
|
||||
const [showRouting, setShowRouting] = useState(false);
|
||||
|
||||
const uiTags = useMemo(() => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Info } from 'lucide-react';
|
||||
import { useLoaderData } from 'react-router';
|
||||
import Code from '~/components/Code';
|
||||
import Link from '~/components/Link';
|
||||
import Tooltip from '~/components/Tooltip';
|
||||
@@ -67,9 +66,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
||||
|
||||
export const action = machineAction;
|
||||
|
||||
export default function Page() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
export default function Page({ loaderData }: Route.ComponentProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
@@ -86,10 +83,10 @@ export default function Page() {
|
||||
</p>
|
||||
</div>
|
||||
<NewMachine
|
||||
disabledKeys={data.preAuth ? [] : ['pre-auth']}
|
||||
isDisabled={!data.writable}
|
||||
server={data.publicServer ?? data.server}
|
||||
users={data.users}
|
||||
disabledKeys={loaderData.preAuth ? [] : ['pre-auth']}
|
||||
isDisabled={!loaderData.writable}
|
||||
server={loaderData.publicServer ?? loaderData.server}
|
||||
users={loaderData.users}
|
||||
/>
|
||||
</div>
|
||||
<table className="table-auto w-full rounded-lg">
|
||||
@@ -99,7 +96,7 @@ export default function Page() {
|
||||
<th className="pb-2 w-1/4">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<p className="uppercase text-xs font-bold">Addresses</p>
|
||||
{data.magic ? (
|
||||
{loaderData.magic ? (
|
||||
<Tooltip>
|
||||
<Info className="w-4 h-4" />
|
||||
<Tooltip.Body className="font-normal">
|
||||
@@ -107,7 +104,7 @@ export default function Page() {
|
||||
their name and also at{' '}
|
||||
<Code>
|
||||
[name].
|
||||
{data.magic}
|
||||
{loaderData.magic}
|
||||
</Code>
|
||||
</Tooltip.Body>
|
||||
</Tooltip>
|
||||
@@ -115,7 +112,7 @@ export default function Page() {
|
||||
</div>
|
||||
</th>
|
||||
{/* We only want to show the version column if there are agents */}
|
||||
{data.agent !== undefined ? (
|
||||
{loaderData.agent !== undefined ? (
|
||||
<th className="uppercase text-xs font-bold pb-2">Version</th>
|
||||
) : undefined}
|
||||
<th className="uppercase text-xs font-bold pb-2">Last Seen</th>
|
||||
@@ -127,18 +124,21 @@ export default function Page() {
|
||||
'border-t border-headplane-100 dark:border-headplane-800',
|
||||
)}
|
||||
>
|
||||
{data.populatedNodes.map((machine) => (
|
||||
{loaderData.populatedNodes.map((node) => (
|
||||
<MachineRow
|
||||
isAgent={data.agent ? data.agent === machine.nodeKey : undefined}
|
||||
isDisabled={
|
||||
data.writable
|
||||
? false // If the user has write permissions, they can edit all machines
|
||||
: machine.user.providerId?.split('/').pop() !== data.subject
|
||||
isAgent={
|
||||
loaderData.agent ? loaderData.agent === node.nodeKey : undefined
|
||||
}
|
||||
key={machine.id}
|
||||
magic={data.magic}
|
||||
node={machine}
|
||||
users={data.users}
|
||||
isDisabled={
|
||||
loaderData.writable
|
||||
? false // If the user has write permissions, they can edit all machines
|
||||
: node.user.providerId?.split('/').pop() !==
|
||||
loaderData.subject
|
||||
}
|
||||
key={node.id}
|
||||
magic={loaderData.magic}
|
||||
node={node}
|
||||
users={loaderData.users}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
+20
-43
@@ -4,20 +4,11 @@ import { faker } from '@faker-js/faker';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
ActionFunctionArgs,
|
||||
data,
|
||||
LinksFunction,
|
||||
LoaderFunctionArgs,
|
||||
ShouldRevalidateFunction,
|
||||
useLoaderData,
|
||||
useSubmit,
|
||||
} from 'react-router';
|
||||
import { data, type ShouldRevalidateFunction, useSubmit } from 'react-router';
|
||||
import { ExternalScriptsHandle } from 'remix-utils/external-scripts';
|
||||
import { LoadContext } from '~/server';
|
||||
import { EphemeralNodeInsert, ephemeralNodes } from '~/server/db/schema';
|
||||
import { Machine, PreAuthKey, User } from '~/types';
|
||||
import { useLiveData } from '~/utils/live-data';
|
||||
import type { Route } from './+types/console';
|
||||
import UserPrompt from './user-prompt';
|
||||
import XTerm from './xterm.client';
|
||||
|
||||
@@ -25,10 +16,7 @@ export const shouldRevalidate: ShouldRevalidateFunction = () => {
|
||||
return false;
|
||||
};
|
||||
|
||||
export async function loader({
|
||||
request,
|
||||
context,
|
||||
}: LoaderFunctionArgs<LoadContext>) {
|
||||
export async function loader({ request, context }: Route.LoaderArgs) {
|
||||
const origin = new URL(request.url).origin;
|
||||
const assets = ['/wasm_exec.js', '/hp_ssh.wasm'];
|
||||
const missing: string[] = [];
|
||||
@@ -53,10 +41,9 @@ export async function loader({
|
||||
if (session.user.subject === 'unknown-non-oauth') {
|
||||
throw data('Only OAuth users are allowed to use WebSSH', 403);
|
||||
}
|
||||
const { users } = await context.client.get<{ users: User[] }>(
|
||||
'v1/user',
|
||||
session.api_key,
|
||||
);
|
||||
|
||||
const api = context.hsApi.getRuntimeClient(session.api_key);
|
||||
const users = await api.getUsers();
|
||||
|
||||
// MARK: This assumes that a user has authenticated with Headscale first
|
||||
// Since the only way to enforce permissions via ACLs is to generate a
|
||||
@@ -77,15 +64,12 @@ export async function loader({
|
||||
);
|
||||
}
|
||||
|
||||
const { preAuthKey } = await context.client.post<{ preAuthKey: PreAuthKey }>(
|
||||
'v1/preauthkey',
|
||||
session.api_key,
|
||||
{
|
||||
user: lookup.id,
|
||||
reusable: false,
|
||||
ephemeral: true,
|
||||
expiration: new Date(Date.now() + 60 * 1000).toISOString(), // 1 minute
|
||||
},
|
||||
const preAuthKey = await api.createPreAuthKey(
|
||||
lookup.id,
|
||||
true, // ephemeral
|
||||
false, // reusable
|
||||
new Date(Date.now() + 60 * 1000), // expiration: 1 minute
|
||||
null, // aclTags
|
||||
);
|
||||
|
||||
// TODO: Enable config to enforce generate_authkeys capability
|
||||
@@ -130,13 +114,8 @@ export async function loader({
|
||||
// );
|
||||
// }
|
||||
|
||||
const { nodes } = await context.client.get<{ nodes: Machine[] }>(
|
||||
'v1/node',
|
||||
session.api_key,
|
||||
);
|
||||
|
||||
// node.name is the hostname, given_name is the set name
|
||||
const lookupNode = nodes.find((n) => n.name === hostname);
|
||||
const nodes = await api.getNodes();
|
||||
const lookupNode = nodes.find((n) => n.givenName === hostname);
|
||||
if (!lookupNode) {
|
||||
throw data(`Node with hostname ${hostname} not found`, 404);
|
||||
}
|
||||
@@ -182,11 +161,8 @@ function generateHostname(username: string) {
|
||||
return `ssh-${adjective}-${noun}-${username}`;
|
||||
}
|
||||
|
||||
export async function action({
|
||||
request,
|
||||
context,
|
||||
}: ActionFunctionArgs<LoadContext>) {
|
||||
const _session = await context.sessions.auth(request);
|
||||
export async function action({ request, context }: Route.ActionArgs) {
|
||||
await context.sessions.auth(request);
|
||||
if (!context.agents?.agentID()) {
|
||||
throw data(
|
||||
'WebSSH is only available with the Headplane agent integration',
|
||||
@@ -214,7 +190,7 @@ export async function action({
|
||||
.where(eq(ephemeralNodes.auth_key, authKey));
|
||||
}
|
||||
|
||||
export const links: LinksFunction = () => [
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{
|
||||
rel: 'preload',
|
||||
href: '/hp_ssh.wasm',
|
||||
@@ -234,13 +210,14 @@ export const handle: ExternalScriptsHandle = {
|
||||
],
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
export default function Page({
|
||||
loaderData: { ipnDetails, sshDetails },
|
||||
}: Route.ComponentProps) {
|
||||
const submit = useSubmit();
|
||||
const { pause } = useLiveData();
|
||||
|
||||
const [ipn, setIpn] = useState<TsWasmNet | null>(null);
|
||||
const [nodeKey, setNodeKey] = useState<string | null>(null);
|
||||
const { ipnDetails, sshDetails } = useLoaderData<typeof loader>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!ipnDetails) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { LoaderFunctionArgs } from 'react-router';
|
||||
import type { LoadContext } from '~/server';
|
||||
import type { Route } from './+types/healthz';
|
||||
|
||||
export async function loader({ context }: Route.LoaderArgs) {
|
||||
// Use a fake API key for healthcheck
|
||||
const api = context.hsApi.getRuntimeClient('fake-api-key');
|
||||
const healthy = await api.isHealthy();
|
||||
|
||||
export async function loader({ context }: LoaderFunctionArgs<LoadContext>) {
|
||||
const healthy = await context.client.healthcheck();
|
||||
return new Response(JSON.stringify({ status: healthy ? 'OK' : 'ERROR' }), {
|
||||
status: healthy ? 200 : 500,
|
||||
headers: {
|
||||
|
||||
Reference in New Issue
Block a user