mirror of
https://github.com/tale/headplane.git
synced 2026-08-18 09:03:14 +00:00
feat: support readonly config and no docker sock
This commit is contained in:
@@ -27,9 +27,11 @@ import TableList from '~/components/TableList'
|
||||
type Properties = {
|
||||
readonly baseDomain?: string;
|
||||
readonly searchDomains: string[];
|
||||
// eslint-disable-next-line react/boolean-prop-naming
|
||||
readonly disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function Domains({ baseDomain, searchDomains }: Properties) {
|
||||
export default function Domains({ baseDomain, searchDomains, disabled }: Properties) {
|
||||
// eslint-disable-next-line unicorn/no-null, @typescript-eslint/ban-types
|
||||
const [activeId, setActiveId] = useState<number | string | null>(null)
|
||||
const [localDomains, setLocalDomains] = useState(searchDomains)
|
||||
@@ -88,8 +90,14 @@ export default function Domains({ baseDomain, searchDomains }: Properties) {
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{localDomains.map((sd, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Domain key={index} domain={sd} id={index + 1} localDomains={localDomains}/>
|
||||
<Domain
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={index}
|
||||
domain={sd}
|
||||
id={index + 1}
|
||||
localDomains={localDomains}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
<DragOverlay adjustScale>
|
||||
{activeId ? <Domain
|
||||
@@ -97,38 +105,41 @@ export default function Domains({ baseDomain, searchDomains }: Properties) {
|
||||
domain={localDomains[activeId as number - 1]}
|
||||
localDomains={localDomains}
|
||||
id={activeId as number - 1}
|
||||
disabled={disabled}
|
||||
/> : undefined}
|
||||
</DragOverlay>
|
||||
</SortableContext>
|
||||
<TableList.Item key='add-sd'>
|
||||
<Input
|
||||
variant='embedded'
|
||||
type='text'
|
||||
className='font-mono text-sm'
|
||||
placeholder='Search Domain'
|
||||
value={newDomain}
|
||||
onChange={event => {
|
||||
setNewDomain(event.target.value)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className='text-sm'
|
||||
disabled={newDomain.length === 0}
|
||||
onClick={() => {
|
||||
fetcher.submit({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'dns_config.domains': [...localDomains, newDomain]
|
||||
}, {
|
||||
method: 'PATCH',
|
||||
encType: 'application/json'
|
||||
})
|
||||
{disabled ? undefined : (
|
||||
<TableList.Item key='add-sd'>
|
||||
<Input
|
||||
variant='embedded'
|
||||
type='text'
|
||||
className='font-mono text-sm'
|
||||
placeholder='Search Domain'
|
||||
value={newDomain}
|
||||
onChange={event => {
|
||||
setNewDomain(event.target.value)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className='text-sm'
|
||||
disabled={newDomain.length === 0}
|
||||
onClick={() => {
|
||||
fetcher.submit({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'dns_config.domains': [...localDomains, newDomain]
|
||||
}, {
|
||||
method: 'PATCH',
|
||||
encType: 'application/json'
|
||||
})
|
||||
|
||||
setNewDomain('')
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</TableList.Item>
|
||||
setNewDomain('')
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</TableList.Item>
|
||||
)}
|
||||
</TableList>
|
||||
</DndContext>
|
||||
</div>
|
||||
@@ -140,9 +151,11 @@ type DomainProperties = {
|
||||
readonly id: number;
|
||||
readonly isDrag?: boolean;
|
||||
readonly localDomains: string[];
|
||||
// eslint-disable-next-line react/boolean-prop-naming
|
||||
readonly disabled?: boolean;
|
||||
}
|
||||
|
||||
function Domain({ domain, id, localDomains, isDrag }: DomainProperties) {
|
||||
function Domain({ domain, id, localDomains, isDrag, disabled }: DomainProperties) {
|
||||
const fetcher = useFetcher()
|
||||
|
||||
const {
|
||||
@@ -169,17 +182,20 @@ function Domain({ domain, id, localDomains, isDrag }: DomainProperties) {
|
||||
}}
|
||||
>
|
||||
<p className='font-mono text-sm flex items-center gap-4'>
|
||||
<Bars3Icon
|
||||
className='h-4 w-4 text-gray-400 focus:outline-none'
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
{disabled ? undefined : (
|
||||
<Bars3Icon
|
||||
className='h-4 w-4 text-gray-400 focus:outline-none'
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
)}
|
||||
{domain}
|
||||
</p>
|
||||
{isDrag ? undefined : (
|
||||
<Button
|
||||
variant='destructive'
|
||||
className='text-sm'
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
fetcher.submit({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
|
||||
@@ -7,9 +7,11 @@ import Button from '~/components/Button'
|
||||
|
||||
type Properties = {
|
||||
readonly isEnabled: boolean;
|
||||
// eslint-disable-next-line react/boolean-prop-naming
|
||||
readonly disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function Modal({ isEnabled }: Properties) {
|
||||
export default function Modal({ isEnabled, disabled }: Properties) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const fetcher = useFetcher()
|
||||
|
||||
@@ -18,6 +20,7 @@ export default function Modal({ isEnabled }: Properties) {
|
||||
<Button
|
||||
variant='emphasized'
|
||||
className='w-fit text-sm'
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setIsOpen(true)
|
||||
}}
|
||||
|
||||
@@ -10,9 +10,11 @@ import Input from '~/components/Input'
|
||||
|
||||
type Properties = {
|
||||
readonly name: string;
|
||||
// eslint-disable-next-line react/boolean-prop-naming
|
||||
readonly disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function Modal({ name }: Properties) {
|
||||
export default function Modal({ name, disabled }: Properties) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [newName, setNewName] = useState(name)
|
||||
const fetcher = useFetcher()
|
||||
@@ -32,7 +34,7 @@ export default function Modal({ name }: Properties) {
|
||||
</p>
|
||||
<Input
|
||||
readOnly
|
||||
className='font-mono text-sm my-4'
|
||||
className='font-mono text-sm my-4 w-1/2'
|
||||
type='text'
|
||||
value={name}
|
||||
onFocus={event => {
|
||||
@@ -42,6 +44,7 @@ export default function Modal({ name }: Properties) {
|
||||
<Button
|
||||
variant='emphasized'
|
||||
className='text-sm w-fit'
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
setIsOpen(true)
|
||||
}}
|
||||
|
||||
@@ -7,8 +7,9 @@ import { useState } from 'react'
|
||||
import Button from '~/components/Button'
|
||||
import Code from '~/components/Code'
|
||||
import Input from '~/components/Input'
|
||||
import Notice from '~/components/Notice'
|
||||
import TableList from '~/components/TableList'
|
||||
import { getConfig, patchConfig } from '~/utils/config'
|
||||
import { getConfig, getContext, patchConfig } from '~/utils/config'
|
||||
import { restartHeadscale } from '~/utils/docker'
|
||||
|
||||
import Domains from './domains'
|
||||
@@ -18,6 +19,8 @@ import RenameModal from './rename'
|
||||
// We do not want to expose every config value
|
||||
export async function loader() {
|
||||
const config = await getConfig()
|
||||
const context = await getContext()
|
||||
|
||||
const dns = {
|
||||
prefixes: config.prefixes,
|
||||
magicDns: config.dns_config.magic_dns,
|
||||
@@ -29,10 +32,18 @@ export async function loader() {
|
||||
extraRecords: config.dns_config.extra_records
|
||||
}
|
||||
|
||||
return dns
|
||||
return {
|
||||
...dns,
|
||||
...context
|
||||
}
|
||||
}
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const context = await getContext()
|
||||
if (!context.hasConfigWrite) {
|
||||
return json({ success: false })
|
||||
}
|
||||
|
||||
const data = await request.json() as Record<string, unknown>
|
||||
await patchConfig(data)
|
||||
await restartHeadscale()
|
||||
@@ -47,7 +58,12 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-16 max-w-screen-lg'>
|
||||
<RenameModal name={data.baseDomain}/>
|
||||
{data.hasConfigWrite ? undefined : (
|
||||
<Notice>
|
||||
The Headscale configuration is read-only. You cannot make changes to the configuration
|
||||
</Notice>
|
||||
)}
|
||||
<RenameModal name={data.baseDomain} disabled={!data.hasConfigWrite}/>
|
||||
<div className='flex flex-col w-2/3'>
|
||||
<h1 className='text-2xl font-medium mb-4'>Nameservers</h1>
|
||||
<p className='text-gray-700 dark:text-gray-300'>
|
||||
@@ -63,6 +79,7 @@ export default function Page() {
|
||||
<span className='text-sm opacity-50'>Override local DNS</span>
|
||||
<Switch
|
||||
checked={localOverride}
|
||||
disabled={!data.hasConfigWrite}
|
||||
className={clsx(
|
||||
localOverride ? 'bg-gray-800' : 'bg-gray-200',
|
||||
'relative inline-flex h-4 w-9 items-center rounded-full'
|
||||
@@ -97,6 +114,7 @@ export default function Page() {
|
||||
<Button
|
||||
variant='destructive'
|
||||
className='text-sm'
|
||||
disabled={!data.hasConfigWrite}
|
||||
onClick={() => {
|
||||
fetcher.submit({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -111,35 +129,37 @@ export default function Page() {
|
||||
</Button>
|
||||
</TableList.Item>
|
||||
))}
|
||||
<TableList.Item>
|
||||
<Input
|
||||
variant='embedded'
|
||||
type='text'
|
||||
className='font-mono text-sm'
|
||||
placeholder='Nameserver'
|
||||
value={ns}
|
||||
onChange={event => {
|
||||
setNs(event.target.value)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className='text-sm'
|
||||
disabled={ns.length === 0}
|
||||
onClick={() => {
|
||||
fetcher.submit({
|
||||
{data.hasConfigWrite ? (
|
||||
<TableList.Item>
|
||||
<Input
|
||||
variant='embedded'
|
||||
type='text'
|
||||
className='font-mono text-sm'
|
||||
placeholder='Nameserver'
|
||||
value={ns}
|
||||
onChange={event => {
|
||||
setNs(event.target.value)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className='text-sm'
|
||||
disabled={ns.length === 0}
|
||||
onClick={() => {
|
||||
fetcher.submit({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'dns_config.nameservers': [...data.nameservers, ns]
|
||||
}, {
|
||||
method: 'PATCH',
|
||||
encType: 'application/json'
|
||||
})
|
||||
'dns_config.nameservers': [...data.nameservers, ns]
|
||||
}, {
|
||||
method: 'PATCH',
|
||||
encType: 'application/json'
|
||||
})
|
||||
|
||||
setNs('')
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</TableList.Item>
|
||||
setNs('')
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</TableList.Item>
|
||||
) : undefined}
|
||||
</TableList>
|
||||
{/* TODO: Split DNS and Custom A Records */}
|
||||
</div>
|
||||
@@ -148,6 +168,7 @@ export default function Page() {
|
||||
<Domains
|
||||
baseDomain={data.magicDns ? data.baseDomain : undefined}
|
||||
searchDomains={data.searchDomains}
|
||||
disabled={!data.hasConfigWrite}
|
||||
/>
|
||||
|
||||
<div className='flex flex-col w-2/3'>
|
||||
@@ -162,7 +183,7 @@ export default function Page() {
|
||||
{' '}
|
||||
when Magic DNS is enabled.
|
||||
</p>
|
||||
<MagicModal isEnabled={data.magicDns}/>
|
||||
<MagicModal isEnabled={data.magicDns} disabled={!data.hasConfigWrite}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user