mirror of
https://github.com/tale/headplane.git
synced 2026-08-30 08:49:30 +00:00
fix: resolve random css issues with mobile layouts
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
- Added an `/admin/api/info` route that can expose sensitive information if `server.info_secret` is set in the configuration (closes [#324](https://github.com/tale/headplane/issues/324)).
|
- Added an `/admin/api/info` route that can expose sensitive information if `server.info_secret` is set in the configuration (closes [#324](https://github.com/tale/headplane/issues/324)).
|
||||||
- Correctly apply Gravatar profile pictures on the user page if applicable (closes [#405](https://github.com/tale/headplane/issues/405)).
|
- Correctly apply Gravatar profile pictures on the user page if applicable (closes [#405](https://github.com/tale/headplane/issues/405)).
|
||||||
- Machine key registration no longer works if the key isn't 24 characters long (closes [#415](https://github.com/tale/headplane/issues/415)).
|
- Machine key registration no longer works if the key isn't 24 characters long (closes [#415](https://github.com/tale/headplane/issues/415)).
|
||||||
|
- Fixed some mobile CSS issues across the application (closes [#401](https://github.com/tale/headplane/issues/401)).
|
||||||
---
|
---
|
||||||
|
|
||||||
# 0.6.1 (October 12, 2025)
|
# 0.6.1 (October 12, 2025)
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ function Tabs({ label, className, ...props }: TabsProps) {
|
|||||||
<div className={cn('flex flex-col', className)}>
|
<div className={cn('flex flex-col', className)}>
|
||||||
<div
|
<div
|
||||||
{...tabListProps}
|
{...tabListProps}
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center rounded-t-xl w-fit',
|
'flex items-center rounded-t-xl w-fit max-w-full overflow-x-auto',
|
||||||
'border-headplane-100 dark:border-headplane-800',
|
'border-headplane-100 dark:border-headplane-800',
|
||||||
'border-t border-x',
|
'border-t border-x',
|
||||||
)}
|
)}
|
||||||
|
ref={ref}
|
||||||
>
|
>
|
||||||
{[...state.collection].map((item) => (
|
{[...state.collection].map((item) => (
|
||||||
<Tab key={item.key} item={item} state={state} />
|
<Tab item={item} key={item.key} state={state} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<TabsPanel key={state.selectedItem?.key} state={state} />
|
<TabsPanel key={state.selectedItem?.key} state={state} />
|
||||||
@@ -52,7 +52,6 @@ function Tab({ item, state }: TabsTabProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...tabProps}
|
{...tabProps}
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'pl-2 pr-3 py-2.5',
|
'pl-2 pr-3 py-2.5',
|
||||||
'aria-selected:bg-headplane-100 dark:aria-selected:bg-headplane-950',
|
'aria-selected:bg-headplane-100 dark:aria-selected:bg-headplane-950',
|
||||||
@@ -60,6 +59,7 @@ function Tab({ item, state }: TabsTabProps) {
|
|||||||
'border-r border-headplane-100 dark:border-headplane-800',
|
'border-r border-headplane-100 dark:border-headplane-800',
|
||||||
'first:rounded-tl-xl last:rounded-tr-xl last:border-r-0',
|
'first:rounded-tl-xl last:rounded-tr-xl last:border-r-0',
|
||||||
)}
|
)}
|
||||||
|
ref={ref}
|
||||||
>
|
>
|
||||||
{rendered}
|
{rendered}
|
||||||
</div>
|
</div>
|
||||||
@@ -76,11 +76,11 @@ function TabsPanel({ state, ...props }: TabsPanelProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...tabPanelProps}
|
{...tabPanelProps}
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full overflow-clip rounded-b-xl rounded-r-xl',
|
'w-full overflow-clip rounded-b-xl rounded-r-xl',
|
||||||
'border border-headplane-100 dark:border-headplane-800',
|
'border border-headplane-100 dark:border-headplane-800',
|
||||||
)}
|
)}
|
||||||
|
ref={ref}
|
||||||
>
|
>
|
||||||
{state.selectedItem?.props.children}
|
{state.selectedItem?.props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@ export function Layout({ children }: { readonly children: React.ReactNode }) {
|
|||||||
<Links />
|
<Links />
|
||||||
<link href="favicon.ico" rel="icon" />
|
<link href="favicon.ico" rel="icon" />
|
||||||
</head>
|
</head>
|
||||||
<body className="overscroll-none dark:bg-headplane-900 dark:text-headplane-50">
|
<body className="overscroll-none overflow-x-hidden dark:bg-headplane-900 dark:text-headplane-50">
|
||||||
{children}
|
{children}
|
||||||
<ToastProvider queue={toastQueue} />
|
<ToastProvider queue={toastQueue} />
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { DndContext, DragOverlay, closestCorners } from '@dnd-kit/core';
|
import { closestCorners, DndContext, DragOverlay } from '@dnd-kit/core';
|
||||||
import {
|
import {
|
||||||
restrictToParentElement,
|
restrictToParentElement,
|
||||||
restrictToVerticalAxis,
|
restrictToVerticalAxis,
|
||||||
} from '@dnd-kit/modifiers';
|
} from '@dnd-kit/modifiers';
|
||||||
import {
|
import {
|
||||||
SortableContext,
|
|
||||||
arrayMove,
|
arrayMove,
|
||||||
|
SortableContext,
|
||||||
useSortable,
|
useSortable,
|
||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { GripVertical, Lock } from 'lucide-react';
|
import { GripVertical, Lock } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { type FetcherWithComponents, Form, useFetcher } from 'react-router';
|
import { Form } from 'react-router';
|
||||||
import Button from '~/components/Button';
|
import Button from '~/components/Button';
|
||||||
import Input from '~/components/Input';
|
import Input from '~/components/Input';
|
||||||
import TableList from '~/components/TableList';
|
import TableList from '~/components/TableList';
|
||||||
@@ -37,18 +37,15 @@ export default function ManageDomains({
|
|||||||
}, [searchDomains]);
|
}, [searchDomains]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col w-full sm:w-2/3">
|
||||||
<h1 className="text-2xl font-medium mb-4">Search Domains</h1>
|
<h1 className="text-2xl font-medium mb-4">Search Domains</h1>
|
||||||
<p className="mb-4">
|
<p className="mb-4">
|
||||||
Set custom DNS search domains for your Tailnet. When using Magic DNS,
|
Set custom DNS search domains for your Tailnet. When using Magic DNS,
|
||||||
your tailnet domain is used as the first search domain.
|
your tailnet domain is used as the first search domain.
|
||||||
</p>
|
</p>
|
||||||
<DndContext
|
<DndContext
|
||||||
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
|
|
||||||
collisionDetection={closestCorners}
|
collisionDetection={closestCorners}
|
||||||
onDragStart={(event) => {
|
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
|
||||||
setActiveId(event.active.id);
|
|
||||||
}}
|
|
||||||
onDragEnd={(event) => {
|
onDragEnd={(event) => {
|
||||||
setActiveId(null);
|
setActiveId(null);
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
@@ -70,6 +67,9 @@ export default function ManageDomains({
|
|||||||
setLocalDomains(arrayMove(localDomains, oldIndex, newIndex));
|
setLocalDomains(arrayMove(localDomains, oldIndex, newIndex));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onDragStart={(event) => {
|
||||||
|
setActiveId(event.active.id);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<TableList>
|
<TableList>
|
||||||
{magic ? (
|
{magic ? (
|
||||||
@@ -91,19 +91,19 @@ export default function ManageDomains({
|
|||||||
>
|
>
|
||||||
{localDomains.map((sd, index) => (
|
{localDomains.map((sd, index) => (
|
||||||
<Domain
|
<Domain
|
||||||
key={sd}
|
|
||||||
domain={sd}
|
domain={sd}
|
||||||
id={index + 1}
|
id={index + 1}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
|
key={sd}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<DragOverlay adjustScale>
|
<DragOverlay adjustScale>
|
||||||
{activeId ? (
|
{activeId ? (
|
||||||
<Domain
|
<Domain
|
||||||
isDragging
|
|
||||||
domain={localDomains[(activeId as number) - 1]}
|
domain={localDomains[(activeId as number) - 1]}
|
||||||
id={(activeId as number) - 1}
|
id={(activeId as number) - 1}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
|
isDragging
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
@@ -111,28 +111,28 @@ export default function ManageDomains({
|
|||||||
{isDisabled ? undefined : (
|
{isDisabled ? undefined : (
|
||||||
<TableList.Item key="add-sd">
|
<TableList.Item key="add-sd">
|
||||||
<Form
|
<Form
|
||||||
method="POST"
|
|
||||||
className="flex items-center justify-between w-full"
|
className="flex items-center justify-between w-full"
|
||||||
|
method="POST"
|
||||||
>
|
>
|
||||||
<input type="hidden" name="action_id" value="add_domain" />
|
<input name="action_id" type="hidden" value="add_domain" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-none font-mono p-0 text-sm',
|
'border-none font-mono p-0 text-sm',
|
||||||
'rounded-none focus:ring-0 w-full ml-1',
|
'rounded-none focus:ring-0 w-full ml-1',
|
||||||
)}
|
)}
|
||||||
placeholder="Search Domain"
|
|
||||||
label="Search Domain"
|
|
||||||
name="domain"
|
|
||||||
labelHidden
|
|
||||||
isRequired
|
isRequired
|
||||||
|
label="Search Domain"
|
||||||
|
labelHidden
|
||||||
|
name="domain"
|
||||||
|
placeholder="Search Domain"
|
||||||
|
type="text"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-2 py-1 rounded-md',
|
'px-2 py-1 rounded-md',
|
||||||
'text-blue-500 dark:text-blue-400',
|
'text-blue-500 dark:text-blue-400',
|
||||||
)}
|
)}
|
||||||
|
type="submit"
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
@@ -164,11 +164,11 @@ function Domain({ domain, id, isDragging, isDisabled }: DomainProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TableList.Item
|
<TableList.Item
|
||||||
ref={setNodeRef}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
isSortableDragging ? 'opacity-50' : '',
|
isSortableDragging ? 'opacity-50' : '',
|
||||||
isDragging ? 'ring-3 bg-white dark:bg-headplane-900' : '',
|
isDragging ? 'ring-3 bg-white dark:bg-headplane-900' : '',
|
||||||
)}
|
)}
|
||||||
|
ref={setNodeRef}
|
||||||
style={{
|
style={{
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
@@ -186,15 +186,15 @@ function Domain({ domain, id, isDragging, isDisabled }: DomainProps) {
|
|||||||
</p>
|
</p>
|
||||||
{isDragging ? undefined : (
|
{isDragging ? undefined : (
|
||||||
<Form method="POST">
|
<Form method="POST">
|
||||||
<input type="hidden" name="action_id" value="remove_domain" />
|
<input name="action_id" type="hidden" value="remove_domain" />
|
||||||
<input type="hidden" name="domain" value={domain} />
|
<input name="domain" type="hidden" value={domain} />
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-2 py-1 rounded-md',
|
'px-2 py-1 rounded-md',
|
||||||
'text-red-500 dark:text-red-400',
|
'text-red-500 dark:text-red-400',
|
||||||
)}
|
)}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
type="submit"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ export default function ManageNS({
|
|||||||
overrideLocalDns,
|
overrideLocalDns,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col w-full sm:w-2/3">
|
||||||
<h1 className="text-2xl font-medium mb-4">Nameservers</h1>
|
<h1 className="text-2xl font-medium mb-4">Nameservers</h1>
|
||||||
<p>
|
<p>
|
||||||
Set the nameservers used by devices on the Tailnet to resolve DNS
|
Set the nameservers used by devices on the Tailnet to resolve DNS
|
||||||
queries.{' '}
|
queries.{' '}
|
||||||
<Link
|
<Link
|
||||||
to="https://tailscale.com/kb/1054/dns"
|
|
||||||
name="Tailscale DNS Documentation"
|
name="Tailscale DNS Documentation"
|
||||||
|
to="https://tailscale.com/kb/1054/dns"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
</Link>
|
</Link>
|
||||||
@@ -35,12 +35,12 @@ export default function ManageNS({
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
{Object.keys(nameservers).map((key) => (
|
{Object.keys(nameservers).map((key) => (
|
||||||
<NameserverList
|
<NameserverList
|
||||||
key={key}
|
|
||||||
isGlobal={key === 'global'}
|
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
|
isGlobal={key === 'global'}
|
||||||
|
key={key}
|
||||||
|
name={key}
|
||||||
nameservers={nameservers}
|
nameservers={nameservers}
|
||||||
overrideLocalDns={overrideLocalDns}
|
overrideLocalDns={overrideLocalDns}
|
||||||
name={key}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -66,16 +66,17 @@ function NameserverList({
|
|||||||
name,
|
name,
|
||||||
}: ListProps) {
|
}: ListProps) {
|
||||||
const list = isGlobal ? nameservers.global : nameservers[name];
|
const list = isGlobal ? nameservers.global : nameservers[name];
|
||||||
|
const submit = useSubmit();
|
||||||
|
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const submit = useSubmit();
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
{isGlobal ? (
|
{isGlobal ? (
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between w-full gap-2">
|
||||||
<h2 className="text-md font-medium opacity-80">
|
<h2 className="text-md font-medium opacity-80">
|
||||||
Global Nameservers
|
Global Nameservers
|
||||||
</h2>
|
</h2>
|
||||||
@@ -87,8 +88,8 @@ function NameserverList({
|
|||||||
names outside the tailnet. When disabled (default), devices
|
names outside the tailnet. When disabled (default), devices
|
||||||
will prefer their local DNS configuration.
|
will prefer their local DNS configuration.
|
||||||
<Link
|
<Link
|
||||||
to="https://tailscale.com/kb/1054/dns#global-nameservers"
|
|
||||||
name="Tailscale Global Nameservers Documentation"
|
name="Tailscale Global Nameservers Documentation"
|
||||||
|
to="https://tailscale.com/kb/1054/dns#global-nameservers"
|
||||||
>
|
>
|
||||||
Learn More
|
Learn More
|
||||||
</Link>
|
</Link>
|
||||||
@@ -96,11 +97,10 @@ function NameserverList({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
<p>Override DNS servers</p>
|
<p>Override DNS servers</p>
|
||||||
<Switch
|
<Switch
|
||||||
label="Override local DNS settings"
|
className="h-[15px] w-[23px] p-0.5"
|
||||||
className="h-[15px] w-[23px] p-[2px]"
|
|
||||||
switchClassName="h-[9px] w-[9px]"
|
|
||||||
name="override_dns"
|
|
||||||
defaultSelected={overrideLocalDns}
|
defaultSelected={overrideLocalDns}
|
||||||
|
label="Override local DNS settings"
|
||||||
|
name="override_dns"
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
submit(
|
submit(
|
||||||
{
|
{
|
||||||
@@ -112,6 +112,7 @@ function NameserverList({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
switchClassName="h-[9px] w-[9px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,20 +126,20 @@ function NameserverList({
|
|||||||
<TableList.Item key={ns}>
|
<TableList.Item key={ns}>
|
||||||
<p className="font-mono text-sm">{ns}</p>
|
<p className="font-mono text-sm">{ns}</p>
|
||||||
<Form method="POST">
|
<Form method="POST">
|
||||||
<input type="hidden" name="action_id" value="remove_ns" />
|
<input name="action_id" type="hidden" value="remove_ns" />
|
||||||
<input type="hidden" name="ns" value={ns} />
|
<input name="ns" type="hidden" value={ns} />
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
|
||||||
name="split_name"
|
name="split_name"
|
||||||
|
type="hidden"
|
||||||
value={isGlobal ? 'global' : name}
|
value={isGlobal ? 'global' : name}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isDisabled}
|
|
||||||
type="submit"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-2 py-1 rounded-md',
|
'px-2 py-1 rounded-md',
|
||||||
'text-red-500 dark:text-red-400',
|
'text-red-500 dark:text-red-400',
|
||||||
)}
|
)}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
type="submit"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ interface Props {
|
|||||||
|
|
||||||
export default function ManageRecords({ records, isDisabled }: Props) {
|
export default function ManageRecords({ records, isDisabled }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col w-full sm:w-2/3">
|
||||||
<h1 className="text-2xl font-medium mb-4">DNS Records</h1>
|
<h1 className="text-2xl font-medium mb-4">DNS Records</h1>
|
||||||
<p>
|
<p>
|
||||||
Headscale supports adding custom DNS records to your Tailnet. As of now,
|
Headscale supports adding custom DNS records to your Tailnet. As of now,
|
||||||
only <Code>A</Code> and <Code>AAAA</Code> records are supported.{' '}
|
only <Code>A</Code> and <Code>AAAA</Code> records are supported.{' '}
|
||||||
<Link
|
<Link
|
||||||
to="https://headscale.net/stable/ref/dns"
|
|
||||||
name="Headscale DNS Records documentation"
|
name="Headscale DNS Records documentation"
|
||||||
|
to="https://headscale.net/stable/ref/dns"
|
||||||
>
|
>
|
||||||
Learn More
|
Learn More
|
||||||
</Link>
|
</Link>
|
||||||
@@ -32,7 +32,7 @@ export default function ManageRecords({ records, isDisabled }: Props) {
|
|||||||
<p className="opacity-50 mx-auto">No DNS records found</p>
|
<p className="opacity-50 mx-auto">No DNS records found</p>
|
||||||
</TableList.Item>
|
</TableList.Item>
|
||||||
) : (
|
) : (
|
||||||
records.map((record, index) => (
|
records.map((record) => (
|
||||||
<TableList.Item key={`${record.name}-${record.value}`}>
|
<TableList.Item key={`${record.name}-${record.value}`}>
|
||||||
<div className="flex gap-2 items-center w-full">
|
<div className="flex gap-2 items-center w-full">
|
||||||
<p
|
<p
|
||||||
@@ -43,22 +43,24 @@ export default function ManageRecords({ records, isDisabled }: Props) {
|
|||||||
>
|
>
|
||||||
{record.type}
|
{record.type}
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-2 w-full">
|
<div className="flex flex-col sm:flex-row sm:gap-2 flex-1 min-w-0">
|
||||||
<p className="font-mono text-sm">{record.name}</p>
|
<p className="font-mono text-sm truncate">{record.name}</p>
|
||||||
<p className="font-mono text-sm">{record.value}</p>
|
<p className="font-mono text-sm truncate opacity-70 sm:opacity-100">
|
||||||
|
{record.value}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Form method="POST">
|
<Form method="POST">
|
||||||
<input type="hidden" name="action_id" value="remove_record" />
|
<input name="action_id" type="hidden" value="remove_record" />
|
||||||
<input type="hidden" name="record_name" value={record.name} />
|
<input name="record_name" type="hidden" value={record.name} />
|
||||||
<input type="hidden" name="record_type" value={record.type} />
|
<input name="record_type" type="hidden" value={record.type} />
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-2 py-1 rounded-md',
|
'px-2 py-1 rounded-md',
|
||||||
'text-red-500 dark:text-red-400',
|
'text-red-500 dark:text-red-400',
|
||||||
)}
|
)}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
type="submit"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -9,21 +9,21 @@ interface Props {
|
|||||||
|
|
||||||
export default function RenameTailnet({ name, isDisabled }: Props) {
|
export default function RenameTailnet({ name, isDisabled }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-2/3 gap-y-4">
|
<div className="flex flex-col w-full sm:w-2/3 gap-y-4">
|
||||||
<h1 className="text-2xl font-medium mb-2">Tailnet Name</h1>
|
<h1 className="text-2xl font-medium mb-2">Tailnet Name</h1>
|
||||||
<p>
|
<p>
|
||||||
This is the base domain name of your Tailnet. Devices are accessible at{' '}
|
This is the base domain name of your Tailnet. Devices are accessible at{' '}
|
||||||
<Code>[device].{name}</Code> when Magic DNS is enabled.
|
<Code>[device].{name}</Code> when Magic DNS is enabled.
|
||||||
</p>
|
</p>
|
||||||
<Input
|
<Input
|
||||||
isReadOnly
|
|
||||||
labelHidden
|
|
||||||
className="w-3/5 font-medium text-sm"
|
className="w-3/5 font-medium text-sm"
|
||||||
|
isReadOnly
|
||||||
label="Tailnet name"
|
label="Tailnet name"
|
||||||
value={name}
|
labelHidden
|
||||||
onFocus={(event) => {
|
onFocus={(event) => {
|
||||||
event.target.select();
|
event.target.select();
|
||||||
}}
|
}}
|
||||||
|
value={name}
|
||||||
/>
|
/>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<Dialog.Button isDisabled={isDisabled}>Rename Tailnet</Dialog.Button>
|
<Dialog.Button isDisabled={isDisabled}>Rename Tailnet</Dialog.Button>
|
||||||
@@ -33,13 +33,13 @@ export default function RenameTailnet({ name, isDisabled }: Props) {
|
|||||||
Keep in mind that changing this can lead to all sorts of unexpected
|
Keep in mind that changing this can lead to all sorts of unexpected
|
||||||
behavior and may break existing devices in your tailnet.
|
behavior and may break existing devices in your tailnet.
|
||||||
</Dialog.Text>
|
</Dialog.Text>
|
||||||
<input type="hidden" name="action_id" value="rename_tailnet" />
|
<input name="action_id" type="hidden" value="rename_tailnet" />
|
||||||
<Input
|
<Input
|
||||||
|
defaultValue={name}
|
||||||
isRequired
|
isRequired
|
||||||
label="Tailnet name"
|
label="Tailnet name"
|
||||||
placeholder="ts.net"
|
|
||||||
defaultValue={name}
|
|
||||||
name="new_name"
|
name="new_name"
|
||||||
|
placeholder="ts.net"
|
||||||
/>
|
/>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -84,20 +84,20 @@ export default function Page() {
|
|||||||
tailnet.
|
tailnet.
|
||||||
</Notice>
|
</Notice>
|
||||||
)}
|
)}
|
||||||
<RenameTailnet name={data.baseDomain} isDisabled={isDisabled} />
|
<RenameTailnet isDisabled={isDisabled} name={data.baseDomain} />
|
||||||
<ManageNS
|
<ManageNS
|
||||||
nameservers={allNs}
|
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
|
nameservers={allNs}
|
||||||
overrideLocalDns={data.overrideDns}
|
overrideLocalDns={data.overrideDns}
|
||||||
/>
|
/>
|
||||||
<ManageRecords records={data.extraRecords} isDisabled={isDisabled} />
|
<ManageRecords isDisabled={isDisabled} records={data.extraRecords} />
|
||||||
<ManageDomains
|
<ManageDomains
|
||||||
searchDomains={data.searchDomains}
|
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
magic={data.magicDns ? data.baseDomain : undefined}
|
magic={data.magicDns ? data.baseDomain : undefined}
|
||||||
|
searchDomains={data.searchDomains}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col w-full sm:w-2/3">
|
||||||
<h1 className="text-2xl font-medium mb-4">Magic DNS</h1>
|
<h1 className="text-2xl font-medium mb-4">Magic DNS</h1>
|
||||||
<p className="mb-4">
|
<p className="mb-4">
|
||||||
Automatically register domain names for each device on the tailnet.
|
Automatically register domain names for each device on the tailnet.
|
||||||
@@ -108,7 +108,7 @@ export default function Page() {
|
|||||||
</Code>{' '}
|
</Code>{' '}
|
||||||
when Magic DNS is enabled.
|
when Magic DNS is enabled.
|
||||||
</p>
|
</p>
|
||||||
<ToggleMagic isEnabled={data.magicDns} isDisabled={isDisabled} />
|
<ToggleMagic isDisabled={isDisabled} isEnabled={data.magicDns} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+152
-150
@@ -157,8 +157,8 @@ export default function Page({ loaderData }: Route.ComponentProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6">
|
||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col">
|
||||||
<h1 className="text-2xl font-medium mb-2">Machines</h1>
|
<h1 className="text-2xl font-medium mb-2">Machines</h1>
|
||||||
<p>
|
<p>
|
||||||
Manage the devices connected to your Tailnet.{' '}
|
Manage the devices connected to your Tailnet.{' '}
|
||||||
@@ -210,85 +210,13 @@ export default function Page({ loaderData }: Route.ComponentProps) {
|
|||||||
: `${loaderData.populatedNodes.length} machines`}
|
: `${loaderData.populatedNodes.length} machines`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<table className="table-auto w-full rounded-lg">
|
<div className="overflow-x-auto">
|
||||||
<thead className="text-headplane-600 dark:text-headplane-300">
|
<table className="table-auto w-full rounded-lg min-w-[640px]">
|
||||||
<tr className="text-left px-0.5">
|
<thead className="text-headplane-600 dark:text-headplane-300">
|
||||||
<th
|
<tr className="text-left px-0.5">
|
||||||
aria-sort={
|
|
||||||
sortField === 'name'
|
|
||||||
? sortDirection === 'asc'
|
|
||||||
? 'ascending'
|
|
||||||
: 'descending'
|
|
||||||
: 'none'
|
|
||||||
}
|
|
||||||
className="uppercase text-xs font-bold pb-2"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Sort by name"
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-x-1 cursor-pointer',
|
|
||||||
'hover:text-headplane-900 dark:hover:text-headplane-100',
|
|
||||||
)}
|
|
||||||
onClick={() => handleSort('name')}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
{sortField === 'name' &&
|
|
||||||
(sortDirection === 'asc' ? (
|
|
||||||
<ChevronUp className="w-3 h-3" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="w-3 h-3" />
|
|
||||||
))}
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
aria-sort={
|
|
||||||
sortField === 'ip'
|
|
||||||
? sortDirection === 'asc'
|
|
||||||
? 'ascending'
|
|
||||||
: 'descending'
|
|
||||||
: 'none'
|
|
||||||
}
|
|
||||||
className="pb-2 w-1/4"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-x-1">
|
|
||||||
<button
|
|
||||||
aria-label="Sort by IP address"
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-x-1 cursor-pointer uppercase text-xs font-bold',
|
|
||||||
'hover:text-headplane-900 dark:hover:text-headplane-100',
|
|
||||||
)}
|
|
||||||
onClick={() => handleSort('ip')}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Addresses
|
|
||||||
{sortField === 'ip' &&
|
|
||||||
(sortDirection === 'asc' ? (
|
|
||||||
<ChevronUp className="w-3 h-3" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="w-3 h-3" />
|
|
||||||
))}
|
|
||||||
</button>
|
|
||||||
{loaderData.magic ? (
|
|
||||||
<Tooltip>
|
|
||||||
<Info className="w-4 h-4" />
|
|
||||||
<Tooltip.Body className="font-normal">
|
|
||||||
Since MagicDNS is enabled, you can access devices based on
|
|
||||||
their name and also at{' '}
|
|
||||||
<Code>
|
|
||||||
[name].
|
|
||||||
{loaderData.magic}
|
|
||||||
</Code>
|
|
||||||
</Tooltip.Body>
|
|
||||||
</Tooltip>
|
|
||||||
) : undefined}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
{/* We only want to show the version column if there are agents */}
|
|
||||||
{loaderData.agent !== undefined ? (
|
|
||||||
<th
|
<th
|
||||||
aria-sort={
|
aria-sort={
|
||||||
sortField === 'version'
|
sortField === 'name'
|
||||||
? sortDirection === 'asc'
|
? sortDirection === 'asc'
|
||||||
? 'ascending'
|
? 'ascending'
|
||||||
: 'descending'
|
: 'descending'
|
||||||
@@ -297,16 +225,16 @@ export default function Page({ loaderData }: Route.ComponentProps) {
|
|||||||
className="uppercase text-xs font-bold pb-2"
|
className="uppercase text-xs font-bold pb-2"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="Sort by version"
|
aria-label="Sort by name"
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-x-1 cursor-pointer',
|
'flex items-center gap-x-1 cursor-pointer',
|
||||||
'hover:text-headplane-900 dark:hover:text-headplane-100',
|
'hover:text-headplane-900 dark:hover:text-headplane-100',
|
||||||
)}
|
)}
|
||||||
onClick={() => handleSort('version')}
|
onClick={() => handleSort('name')}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Version
|
Name
|
||||||
{sortField === 'version' &&
|
{sortField === 'name' &&
|
||||||
(sortDirection === 'asc' ? (
|
(sortDirection === 'asc' ? (
|
||||||
<ChevronUp className="w-3 h-3" />
|
<ChevronUp className="w-3 h-3" />
|
||||||
) : (
|
) : (
|
||||||
@@ -314,76 +242,150 @@ export default function Page({ loaderData }: Route.ComponentProps) {
|
|||||||
))}
|
))}
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
) : undefined}
|
<th
|
||||||
<th
|
aria-sort={
|
||||||
aria-sort={
|
sortField === 'ip'
|
||||||
sortField === 'lastSeen'
|
? sortDirection === 'asc'
|
||||||
? sortDirection === 'asc'
|
? 'ascending'
|
||||||
? 'ascending'
|
: 'descending'
|
||||||
: 'descending'
|
: 'none'
|
||||||
: 'none'
|
}
|
||||||
}
|
className="pb-2 w-1/4"
|
||||||
className="uppercase text-xs font-bold pb-2"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Sort by last seen"
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-x-1 cursor-pointer',
|
|
||||||
'hover:text-headplane-900 dark:hover:text-headplane-100',
|
|
||||||
)}
|
|
||||||
onClick={() => handleSort('lastSeen')}
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
Last Seen
|
<div className="flex items-center gap-x-1">
|
||||||
{sortField === 'lastSeen' &&
|
<button
|
||||||
(sortDirection === 'asc' ? (
|
aria-label="Sort by IP address"
|
||||||
<ChevronUp className="w-3 h-3" />
|
className={cn(
|
||||||
) : (
|
'flex items-center gap-x-1 cursor-pointer uppercase text-xs font-bold',
|
||||||
<ChevronDown className="w-3 h-3" />
|
'hover:text-headplane-900 dark:hover:text-headplane-100',
|
||||||
))}
|
)}
|
||||||
</button>
|
onClick={() => handleSort('ip')}
|
||||||
</th>
|
type="button"
|
||||||
</tr>
|
>
|
||||||
</thead>
|
Addresses
|
||||||
<tbody
|
{sortField === 'ip' &&
|
||||||
className={cn(
|
(sortDirection === 'asc' ? (
|
||||||
'divide-y divide-headplane-100 dark:divide-headplane-800 align-top',
|
<ChevronUp className="w-3 h-3" />
|
||||||
'border-t border-headplane-100 dark:border-headplane-800',
|
) : (
|
||||||
)}
|
<ChevronDown className="w-3 h-3" />
|
||||||
>
|
))}
|
||||||
{filteredAndSortedNodes.length === 0 ? (
|
</button>
|
||||||
<tr>
|
{loaderData.magic ? (
|
||||||
<td
|
<Tooltip>
|
||||||
className="py-8 text-center text-headplane-500"
|
<Info className="w-4 h-4" />
|
||||||
colSpan={loaderData.agent !== undefined ? 5 : 4}
|
<Tooltip.Body className="font-normal">
|
||||||
|
Since MagicDNS is enabled, you can access devices based
|
||||||
|
on their name and also at{' '}
|
||||||
|
<Code>
|
||||||
|
[name].
|
||||||
|
{loaderData.magic}
|
||||||
|
</Code>
|
||||||
|
</Tooltip.Body>
|
||||||
|
</Tooltip>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
{/* We only want to show the version column if there are agents */}
|
||||||
|
{loaderData.agent !== undefined ? (
|
||||||
|
<th
|
||||||
|
aria-sort={
|
||||||
|
sortField === 'version'
|
||||||
|
? sortDirection === 'asc'
|
||||||
|
? 'ascending'
|
||||||
|
: 'descending'
|
||||||
|
: 'none'
|
||||||
|
}
|
||||||
|
className="uppercase text-xs font-bold pb-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-label="Sort by version"
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-x-1 cursor-pointer',
|
||||||
|
'hover:text-headplane-900 dark:hover:text-headplane-100',
|
||||||
|
)}
|
||||||
|
onClick={() => handleSort('version')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Version
|
||||||
|
{sortField === 'version' &&
|
||||||
|
(sortDirection === 'asc' ? (
|
||||||
|
<ChevronUp className="w-3 h-3" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-3 h-3" />
|
||||||
|
))}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
) : undefined}
|
||||||
|
<th
|
||||||
|
aria-sort={
|
||||||
|
sortField === 'lastSeen'
|
||||||
|
? sortDirection === 'asc'
|
||||||
|
? 'ascending'
|
||||||
|
: 'descending'
|
||||||
|
: 'none'
|
||||||
|
}
|
||||||
|
className="uppercase text-xs font-bold pb-2"
|
||||||
>
|
>
|
||||||
No machines found matching "{searchQuery}"
|
<button
|
||||||
</td>
|
aria-label="Sort by last seen"
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-x-1 cursor-pointer',
|
||||||
|
'hover:text-headplane-900 dark:hover:text-headplane-100',
|
||||||
|
)}
|
||||||
|
onClick={() => handleSort('lastSeen')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Last Seen
|
||||||
|
{sortField === 'lastSeen' &&
|
||||||
|
(sortDirection === 'asc' ? (
|
||||||
|
<ChevronUp className="w-3 h-3" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-3 h-3" />
|
||||||
|
))}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
</thead>
|
||||||
filteredAndSortedNodes.map((node) => (
|
<tbody
|
||||||
<MachineRow
|
className={cn(
|
||||||
existingTags={sortNodeTags(loaderData.nodes)}
|
'divide-y divide-headplane-100 dark:divide-headplane-800 align-top',
|
||||||
isAgent={
|
'border-t border-headplane-100 dark:border-headplane-800',
|
||||||
loaderData.agent
|
)}
|
||||||
? loaderData.agent === node.nodeKey
|
>
|
||||||
: undefined
|
{filteredAndSortedNodes.length === 0 ? (
|
||||||
}
|
<tr>
|
||||||
isDisabled={
|
<td
|
||||||
loaderData.writable
|
className="py-8 text-center text-headplane-500"
|
||||||
? false // If the user has write permissions, they can edit all machines
|
colSpan={loaderData.agent !== undefined ? 5 : 4}
|
||||||
: node.user.providerId?.split('/').pop() !==
|
>
|
||||||
loaderData.subject
|
No machines found matching "{searchQuery}"
|
||||||
}
|
</td>
|
||||||
key={node.id}
|
</tr>
|
||||||
magic={loaderData.magic}
|
) : (
|
||||||
node={node}
|
filteredAndSortedNodes.map((node) => (
|
||||||
users={loaderData.users}
|
<MachineRow
|
||||||
/>
|
existingTags={sortNodeTags(loaderData.nodes)}
|
||||||
))
|
isAgent={
|
||||||
)}
|
loaderData.agent
|
||||||
</tbody>
|
? loaderData.agent === node.nodeKey
|
||||||
</table>
|
: undefined
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function Page({
|
|||||||
}: Route.ComponentProps) {
|
}: Route.ComponentProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8 max-w-(--breakpoint-lg)">
|
<div className="flex flex-col gap-8 max-w-(--breakpoint-lg)">
|
||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col w-full sm:w-2/3">
|
||||||
<h1 className="text-2xl font-medium mb-4">Settings</h1>
|
<h1 className="text-2xl font-medium mb-4">Settings</h1>
|
||||||
<p>
|
<p>
|
||||||
The settings page is still under construction. As I'm able to add more
|
The settings page is still under construction. As I'm able to add more
|
||||||
@@ -23,7 +23,7 @@ export default function Page({
|
|||||||
free to open an issue on the GitHub repository.
|
free to open an issue on the GitHub repository.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col w-full sm:w-2/3">
|
||||||
<h1 className="text-2xl font-medium mb-4">Pre-Auth Keys</h1>
|
<h1 className="text-2xl font-medium mb-4">Pre-Auth Keys</h1>
|
||||||
<p>
|
<p>
|
||||||
Headscale fully supports pre-authentication keys in order to easily
|
Headscale fully supports pre-authentication keys in order to easily
|
||||||
@@ -45,7 +45,7 @@ export default function Page({
|
|||||||
</RemixLink>
|
</RemixLink>
|
||||||
{config && isOidcEnabled ? (
|
{config && isOidcEnabled ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col w-full sm:w-2/3">
|
||||||
<h1 className="text-2xl font-medium mb-4">
|
<h1 className="text-2xl font-medium mb-4">
|
||||||
Authentication Restrictions
|
Authentication Restrictions
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function Page({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 max-w-(--breakpoint-lg)">
|
<div className="flex flex-col gap-4 max-w-(--breakpoint-lg)">
|
||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col w-full sm:w-2/3">
|
||||||
<p className="mb-4 text-md">
|
<p className="mb-4 text-md">
|
||||||
<RemixLink className="font-medium" to="/settings">
|
<RemixLink className="font-medium" to="/settings">
|
||||||
Settings
|
Settings
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function RestrictionTable({
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
}: RestrictionProps) {
|
}: RestrictionProps) {
|
||||||
return (
|
return (
|
||||||
<div className="w-2/3">
|
<div className="w-full sm:w-2/3">
|
||||||
<h2 className="text-2xl font-medium mt-8">
|
<h2 className="text-2xl font-medium mt-8">
|
||||||
Permitted {type.charAt(0).toUpperCase() + type.slice(1)}s
|
Permitted {type.charAt(0).toUpperCase() + type.slice(1)}s
|
||||||
</h2>
|
</h2>
|
||||||
@@ -61,7 +61,7 @@ export default function RestrictionTable({
|
|||||||
) : (
|
) : (
|
||||||
<TableList.Item className="flex flex-col items-center gap-2.5 py-4 opacity-70">
|
<TableList.Item className="flex flex-col items-center gap-2.5 py-4 opacity-70">
|
||||||
{iconForType(type)}
|
{iconForType(type)}
|
||||||
<p className="font-semibold">
|
<p className="font-semibold text-center">
|
||||||
All {type}s are permitted to authenticate.
|
All {type}s are permitted to authenticate.
|
||||||
</p>
|
</p>
|
||||||
</TableList.Item>
|
</TableList.Item>
|
||||||
|
|||||||
@@ -114,32 +114,34 @@ export default function Page({ loaderData }: Route.ComponentProps) {
|
|||||||
Manage the users in your network and their permissions.
|
Manage the users in your network and their permissions.
|
||||||
</p>
|
</p>
|
||||||
<ManageBanner isDisabled={!loaderData.writable} oidc={loaderData.oidc} />
|
<ManageBanner isDisabled={!loaderData.writable} oidc={loaderData.oidc} />
|
||||||
<table className="table-auto w-full rounded-lg">
|
<div className="overflow-x-auto">
|
||||||
<thead className="text-headplane-600 dark:text-headplane-300">
|
<table className="table-auto w-full rounded-lg min-w-[640px]">
|
||||||
<tr className="text-left px-0.5">
|
<thead className="text-headplane-600 dark:text-headplane-300">
|
||||||
<th className="uppercase text-xs font-bold pb-2">User</th>
|
<tr className="text-left px-0.5">
|
||||||
<th className="uppercase text-xs font-bold pb-2">Role</th>
|
<th className="uppercase text-xs font-bold pb-2">User</th>
|
||||||
<th className="uppercase text-xs font-bold pb-2">Created At</th>
|
<th className="uppercase text-xs font-bold pb-2">Role</th>
|
||||||
<th className="uppercase text-xs font-bold pb-2">Last Seen</th>
|
<th className="uppercase text-xs font-bold pb-2">Created At</th>
|
||||||
</tr>
|
<th className="uppercase text-xs font-bold pb-2">Last Seen</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody
|
</thead>
|
||||||
className={cn(
|
<tbody
|
||||||
'divide-y divide-headplane-100 dark:divide-headplane-800 align-top',
|
className={cn(
|
||||||
'border-t border-headplane-100 dark:border-headplane-800',
|
'divide-y divide-headplane-100 dark:divide-headplane-800 align-top',
|
||||||
)}
|
'border-t border-headplane-100 dark:border-headplane-800',
|
||||||
>
|
)}
|
||||||
{users
|
>
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
{users
|
||||||
.map((user) => (
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
<UserRow
|
.map((user) => (
|
||||||
key={user.id}
|
<UserRow
|
||||||
role={loaderData.roles[users.indexOf(user)]}
|
key={user.id}
|
||||||
user={user}
|
role={loaderData.roles[users.indexOf(user)]}
|
||||||
/>
|
user={user}
|
||||||
))}
|
/>
|
||||||
</tbody>
|
))}
|
||||||
</table>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
@utility container {
|
@utility container {
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
padding-inline: 1rem;
|
padding-inline: 1rem;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */
|
/* biome-ignore lint/correctness/noUnknownFunction: Tailwind CSS */
|
||||||
@media (width >= theme(--breakpoint-sm)) {
|
@media (width >= theme(--breakpoint-sm)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user