mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 15:58:14 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f04b17109b | |||
| 0ff9e6fdc3 | |||
| 94174ebcce | |||
| c2fe69ec17 | |||
| b285753b24 |
@@ -8,7 +8,7 @@ This is a relatively tiny Remix app that aims to provide a usable GUI for the He
|
||||
It's still very early in it's development, however these are some of the features that are planned.
|
||||
|
||||
- [ ] Editable tags, machine names, users, etc
|
||||
- [ ] ACL control through Docker integration
|
||||
- [x] ACL control through Docker integration
|
||||
- [x] OIDC based login for the web UI
|
||||
- [x] Automated API key regeneration
|
||||
- [x] Editable headscale configuration
|
||||
@@ -16,6 +16,7 @@ It's still very early in it's development, however these are some of the feature
|
||||
## Deployment
|
||||
- If you run Headscale in a Docker container, see the [Advanced Deployment](/docs/Advanced-Integration.md) guide.
|
||||
- If you run Headscale natively, see the [Basic Deployment](/docs/Basic-Integration.md) guide.
|
||||
- For more configuration options, refer to the [Configuration](/docs/Configuration.md) guide.
|
||||
|
||||
## Contributing
|
||||
If you would like to contribute, please install a relatively modern version of Node.js and PNPM.
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { json } from '@codemirror/lang-json'
|
||||
import { yaml } from '@codemirror/lang-yaml'
|
||||
import { useFetcher } from '@remix-run/react'
|
||||
import { githubDark, githubLight } from '@uiw/codemirror-theme-github'
|
||||
import CodeMirror from '@uiw/react-codemirror'
|
||||
import clsx from 'clsx'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import CodeMirrorMerge from 'react-codemirror-merge'
|
||||
import { toast } from 'react-hot-toast/headless'
|
||||
|
||||
import Button from '~/components/Button'
|
||||
import Spinner from '~/components/Spinner'
|
||||
|
||||
import Fallback from './fallback'
|
||||
|
||||
type EditorProperties = {
|
||||
readonly acl: string;
|
||||
readonly setAcl: (acl: string) => void;
|
||||
@@ -18,12 +21,16 @@ type EditorProperties = {
|
||||
readonly data: {
|
||||
hasAclWrite: boolean;
|
||||
currentAcl: string;
|
||||
aclType: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function Editor({ data, acl, setAcl, mode }: EditorProperties) {
|
||||
const [light, setLight] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const fetcher = useFetcher()
|
||||
const aclType = useMemo(() => data.aclType === 'json' ? json() : yaml(), [data.aclType])
|
||||
|
||||
useEffect(() => {
|
||||
const theme = window.matchMedia('(prefers-color-scheme: light)')
|
||||
@@ -32,48 +39,54 @@ export default function Editor({ data, acl, setAcl, mode }: EditorProperties) {
|
||||
theme.addEventListener('change', theme => {
|
||||
setLight(theme.matches)
|
||||
})
|
||||
|
||||
// Prevents the FOUC
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
<div className={clsx(
|
||||
'border border-gray-200 dark:border-gray-700',
|
||||
'rounded-b-lg rounded-tr-lg mb-2 overflow-hidden'
|
||||
)}
|
||||
>
|
||||
{mode === 'edit' ? (
|
||||
<CodeMirror
|
||||
value={acl}
|
||||
maxHeight='calc(100vh - 20rem)'
|
||||
theme={light ? githubLight : githubDark}
|
||||
extensions={[json()]}
|
||||
readOnly={!data.hasAclWrite}
|
||||
onChange={value => {
|
||||
setAcl(value)
|
||||
}}
|
||||
/>
|
||||
{loading ? (
|
||||
<Fallback acl={acl} where='client'/>
|
||||
) : (
|
||||
<div
|
||||
className='overflow-y-scroll'
|
||||
style={{ height: 'calc(100vh - 20rem)' }}
|
||||
>
|
||||
<CodeMirrorMerge
|
||||
mode === 'edit' ? (
|
||||
<CodeMirror
|
||||
value={acl}
|
||||
className='h-editor text-sm'
|
||||
theme={light ? githubLight : githubDark}
|
||||
orientation='a-b'
|
||||
extensions={[aclType]}
|
||||
readOnly={!data.hasAclWrite}
|
||||
onChange={value => {
|
||||
setAcl(value)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className='overflow-y-scroll'
|
||||
style={{ height: 'calc(100vh - 20rem)' }}
|
||||
>
|
||||
<CodeMirrorMerge.Original
|
||||
readOnly
|
||||
value={data.currentAcl}
|
||||
extensions={[json()]}
|
||||
/>
|
||||
<CodeMirrorMerge.Modified
|
||||
readOnly
|
||||
value={acl}
|
||||
extensions={[json()]}
|
||||
/>
|
||||
</CodeMirrorMerge>
|
||||
</div>
|
||||
<CodeMirrorMerge
|
||||
theme={light ? githubLight : githubDark}
|
||||
orientation='a-b'
|
||||
>
|
||||
<CodeMirrorMerge.Original
|
||||
readOnly
|
||||
value={data.currentAcl}
|
||||
extensions={[aclType]}
|
||||
/>
|
||||
<CodeMirrorMerge.Modified
|
||||
readOnly
|
||||
value={acl}
|
||||
extensions={[aclType]}
|
||||
/>
|
||||
</CodeMirrorMerge>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import clsx from 'clsx'
|
||||
|
||||
import Button from '~/components/Button'
|
||||
|
||||
type FallbackProperties = {
|
||||
readonly acl: string;
|
||||
readonly where: 'client' | 'server';
|
||||
}
|
||||
|
||||
export default function Fallback({ acl, where }: FallbackProperties) {
|
||||
return (
|
||||
<>
|
||||
<div className={clsx(
|
||||
where === 'server' ? 'mb-2 overflow-hidden rounded-tr-lg rounded-b-lg' : '',
|
||||
where === 'server' ? 'border border-gray-200 dark:border-gray-700' : ''
|
||||
)}
|
||||
>
|
||||
<textarea
|
||||
readOnly
|
||||
className={clsx(
|
||||
'w-full h-editor font-mono resize-none',
|
||||
'text-sm text-gray-600 dark:text-gray-300',
|
||||
'pl-10 pt-1 leading-snug'
|
||||
)}
|
||||
value={acl}
|
||||
/>
|
||||
</div>
|
||||
{where === 'server' ? (
|
||||
<>
|
||||
<Button
|
||||
disabled
|
||||
variant='emphasized'
|
||||
className='text-sm w-fit mr-2'
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
disabled
|
||||
variant='emphasized'
|
||||
className={clsx(
|
||||
'text-sm w-fit bg-gray-100 dark:bg-transparent',
|
||||
'border border-gray-200 dark:border-gray-700'
|
||||
)}
|
||||
>
|
||||
Discard Changes
|
||||
</Button>
|
||||
</>
|
||||
) : undefined}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { sighupHeadscale } from '~/utils/docker'
|
||||
import { getSession } from '~/utils/sessions'
|
||||
|
||||
import Editor from './editor'
|
||||
import Fallback from './fallback'
|
||||
|
||||
export async function loader() {
|
||||
const context = await getContext()
|
||||
@@ -20,10 +21,11 @@ export async function loader() {
|
||||
throw new Error('No ACL configuration is available')
|
||||
}
|
||||
|
||||
const acl = await getAcl()
|
||||
const { data, type } = await getAcl()
|
||||
return {
|
||||
hasAclWrite: context.hasAclWrite,
|
||||
currentAcl: acl
|
||||
currentAcl: data,
|
||||
aclType: type
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +46,11 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
|
||||
const data = await request.json() as { acl: string }
|
||||
await patchAcl(data.acl)
|
||||
await sighupHeadscale()
|
||||
|
||||
if (context.hasDockerSock) {
|
||||
await sighupHeadscale()
|
||||
}
|
||||
|
||||
return json({ success: true })
|
||||
}
|
||||
|
||||
@@ -85,7 +91,7 @@ export default function Page() {
|
||||
<Tab.List className={clsx(
|
||||
'flex border-t border-gray-200 dark:border-gray-700',
|
||||
'w-fit rounded-t-lg overflow-hidden',
|
||||
'text-gray-300 dark:text-gray-500'
|
||||
'text-gray-400 dark:text-gray-500'
|
||||
)}
|
||||
>
|
||||
<Tab as={Fragment}>
|
||||
@@ -145,14 +151,14 @@ export default function Page() {
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<ClientOnly>
|
||||
<ClientOnly fallback={<Fallback acl={acl} where='server'/>}>
|
||||
{() => (
|
||||
<Editor data={data} acl={acl} setAcl={setAcl} mode='edit'/>
|
||||
)}
|
||||
</ClientOnly>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<ClientOnly>
|
||||
<ClientOnly fallback={<Fallback acl={acl} where='server'/>}>
|
||||
{() => (
|
||||
<Editor data={data} acl={acl} setAcl={setAcl} mode='diff'/>
|
||||
)}
|
||||
@@ -169,7 +175,7 @@ export default function Page() {
|
||||
<CubeTransparentIcon className='w-24 h-24 text-gray-300 dark:text-gray-500'/>
|
||||
<p className='w-1/2 text-center mt-4'>
|
||||
The Preview rules is very much still a work in progress.
|
||||
It's a bit complicated to implement right now but hopefully it will be available soon.
|
||||
It is a bit complicated to implement right now but hopefully it will be available soon.
|
||||
</p>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
|
||||
+15
-6
@@ -2,7 +2,7 @@ import { type FSWatcher, watch } from 'node:fs'
|
||||
import { access, constants, readFile, writeFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
import { type Document, parseDocument } from 'yaml'
|
||||
import { type Document, parse, parseDocument } from 'yaml'
|
||||
|
||||
type Duration = `${string}s` | `${string}h` | `${string}m` | `${string}d` | `${string}y`
|
||||
|
||||
@@ -141,11 +141,19 @@ export async function getAcl() {
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
return ''
|
||||
return { data: '', type: 'json' }
|
||||
}
|
||||
|
||||
const data = await readFile(path, 'utf8')
|
||||
return data
|
||||
|
||||
// Naive check for YAML over JSON
|
||||
// This is because JSON.parse doesn't support comments
|
||||
try {
|
||||
parse(data)
|
||||
return { data, type: 'yaml' }
|
||||
} catch {
|
||||
return { data, type: 'json' }
|
||||
}
|
||||
}
|
||||
|
||||
// This is so obscenely dangerous, please have a check around it
|
||||
@@ -323,8 +331,6 @@ async function hasAclW() {
|
||||
const config = await getConfig()
|
||||
path = config.acl_policy_path
|
||||
} catch {}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
@@ -335,7 +341,10 @@ async function hasAclW() {
|
||||
path = resolve(path)
|
||||
await access(path, constants.W_OK)
|
||||
return true
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
console.log('Cannot acquire read access to ACL file', error)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -44,3 +44,7 @@ services:
|
||||
|
||||
You may also choose to run it natively with the distributed binaries on the releases page.
|
||||
You'll need to manage running this yourself, and I would recommend making a `systemd` unit.
|
||||
|
||||
## ACL Configuration
|
||||
If you would like to get the web ACL configuration working, you'll need to pass the `ACL_FILE` environment variable.
|
||||
This should point to the path of the ACL file on the Headscale server (ie. `ACL_FILE=/etc/headscale/acl_policy.json`).
|
||||
|
||||
@@ -13,7 +13,7 @@ You can configure Headplane using environment variables.
|
||||
- **`HOST`**: The host to bind the server to (default: `0.0.0.0`).
|
||||
- **`PORT`**: The port to bind the server to (default: `3000`).
|
||||
- **`CONFIG_FILE`**: The path to the Headscale `config.yaml` (default: `/etc/headscale/config.yaml`).
|
||||
- **`ACL_FILE`**: The path to the ACL file (default: `/etc/headscale/acl_policy.json`).
|
||||
- **`ACL_FILE`**: The path to the ACL file (default: `/etc/headscale/acl_policy.json`, not needed if you have `acl_policy_path` in your config).
|
||||
- **`HEADSCALE_CONTAINER`**: The name of the Headscale container (required for Docker integration).
|
||||
|
||||
### SSO/OpenID Connect
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-yaml": "^6.1.1",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
|
||||
Generated
+24
@@ -8,6 +8,9 @@ dependencies:
|
||||
'@codemirror/lang-json':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
'@codemirror/lang-yaml':
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1(@codemirror/view@6.26.3)
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -490,6 +493,19 @@ packages:
|
||||
'@lezer/json': 1.0.2
|
||||
dev: false
|
||||
|
||||
/@codemirror/lang-yaml@6.1.1(@codemirror/view@6.26.3):
|
||||
resolution: {integrity: sha512-HV2NzbK9bbVnjWxwObuZh5FuPCowx51mEfoFT9y3y+M37fA3+pbxx4I7uePuygFzDsAmCTwQSc/kXh/flab4uw==}
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
|
||||
'@codemirror/language': 6.10.1
|
||||
'@codemirror/state': 6.4.1
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/highlight': 1.2.0
|
||||
'@lezer/yaml': 1.0.2
|
||||
transitivePeerDependencies:
|
||||
- '@codemirror/view'
|
||||
dev: false
|
||||
|
||||
/@codemirror/language@6.10.1:
|
||||
resolution: {integrity: sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==}
|
||||
dependencies:
|
||||
@@ -1160,6 +1176,14 @@ packages:
|
||||
'@lezer/common': 1.2.1
|
||||
dev: false
|
||||
|
||||
/@lezer/yaml@1.0.2:
|
||||
resolution: {integrity: sha512-XCkwuxe+eumJ28nA9e1S6XKsXz9W7V/AG+WBiWOtiIuUpKcZ/bHuvN8bLxSDREIcybSRpEd/jvphh4vgm6Ed2g==}
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.1
|
||||
'@lezer/highlight': 1.2.0
|
||||
'@lezer/lr': 1.4.0
|
||||
dev: false
|
||||
|
||||
/@mdx-js/mdx@2.3.0:
|
||||
resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==}
|
||||
dependencies:
|
||||
|
||||
+5
-1
@@ -3,7 +3,11 @@ import type { Config } from 'tailwindcss'
|
||||
export default {
|
||||
content: ['./app/**/*.{js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {}
|
||||
extend: {
|
||||
height: {
|
||||
editor: 'calc(100vh - 20rem)'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
} satisfies Config
|
||||
|
||||
Reference in New Issue
Block a user