mirror of
https://github.com/tale/headplane.git
synced 2026-08-29 08:27:08 +00:00
fix: support jsonc comments and switch to monaco for acls
This commit is contained in:
@@ -1,36 +1,33 @@
|
|||||||
import { json } from '@codemirror/lang-json'
|
import Editor, { DiffEditor, Monaco } from '@monaco-editor/react'
|
||||||
import { yaml } from '@codemirror/lang-yaml'
|
import { useEffect, useState } from 'react'
|
||||||
import { useFetcher } from '@remix-run/react'
|
import { ClientOnly } from 'remix-utils/client-only'
|
||||||
import { githubDark, githubLight } from '@uiw/codemirror-theme-github'
|
|
||||||
import CodeMirror from '@uiw/react-codemirror'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
|
||||||
import CodeMirrorMerge from 'react-codemirror-merge'
|
|
||||||
|
|
||||||
import Button from '~/components/Button'
|
import Fallback from '~/routes/_data.acls._index/fallback'
|
||||||
import Spinner from '~/components/Spinner'
|
import { cn } from '~/utils/cn'
|
||||||
import { toast } from '~/components/Toaster'
|
|
||||||
|
|
||||||
import Fallback from './fallback'
|
interface MonacoProps {
|
||||||
|
variant: 'editor' | 'diff'
|
||||||
interface EditorProperties {
|
language: 'json' | 'yaml'
|
||||||
readonly acl: string
|
value: string
|
||||||
readonly setAcl: (acl: string) => void
|
onChange: (value: string) => void
|
||||||
readonly mode: 'edit' | 'diff'
|
original?: string
|
||||||
|
|
||||||
readonly data: {
|
|
||||||
hasAclWrite: boolean
|
|
||||||
currentAcl: string
|
|
||||||
aclType: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Editor({ data, acl, setAcl, mode }: EditorProperties) {
|
function monacoCallback(monaco: Monaco) {
|
||||||
const [light, setLight] = useState(false)
|
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||||
const [loading, setLoading] = useState(true)
|
validate: true,
|
||||||
|
allowComments: true,
|
||||||
|
schemas: [],
|
||||||
|
enableSchemaRequest: true,
|
||||||
|
trailingCommas: 'ignore',
|
||||||
|
})
|
||||||
|
|
||||||
const fetcher = useFetcher()
|
monaco.languages.register({ id: 'json' })
|
||||||
const aclType = useMemo(() => data.aclType === 'json' ? json() : yaml(), [data.aclType])
|
monaco.languages.register({ id: 'yaml' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MonacoEditor({ value, onChange, variant, original, language }: MonacoProps) {
|
||||||
|
const [light, setLight] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const theme = window.matchMedia('(prefers-color-scheme: light)')
|
const theme = window.matchMedia('(prefers-color-scheme: light)')
|
||||||
@@ -39,87 +36,62 @@ export default function Editor({ data, acl, setAcl, mode }: EditorProperties) {
|
|||||||
theme.addEventListener('change', (theme) => {
|
theme.addEventListener('change', (theme) => {
|
||||||
setLight(theme.matches)
|
setLight(theme.matches)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Prevents the FOUC
|
|
||||||
setLoading(false)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={clsx(
|
<div className={cn(
|
||||||
'border border-gray-200 dark:border-gray-700',
|
'border border-gray-200 dark:border-gray-700',
|
||||||
'rounded-b-lg rounded-tr-lg mb-2 z-10 overflow-x-hidden',
|
'rounded-b-lg rounded-tr-lg mb-2 z-10 overflow-x-hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="overflow-y-scroll h-editor text-sm">
|
<div className="overflow-y-scroll h-editor text-sm">
|
||||||
{loading
|
<ClientOnly fallback={<Fallback acl={value} />}>
|
||||||
? (
|
{() => variant === 'editor'
|
||||||
<Fallback acl={acl} where="client" />
|
? (
|
||||||
)
|
<Editor
|
||||||
: (
|
height="100%"
|
||||||
mode === 'edit'
|
language={language}
|
||||||
? (
|
theme={light ? 'light' : 'vs-dark'}
|
||||||
<CodeMirror
|
value={value}
|
||||||
value={acl}
|
onChange={(updated) => {
|
||||||
theme={light ? githubLight : githubDark}
|
if (!updated) {
|
||||||
extensions={[aclType]}
|
return
|
||||||
readOnly={!data.hasAclWrite}
|
}
|
||||||
onChange={(value) => {
|
|
||||||
setAcl(value)
|
if (updated !== value) {
|
||||||
}}
|
onChange(updated)
|
||||||
/>
|
}
|
||||||
)
|
}}
|
||||||
: (
|
loading={<Fallback acl={value} />}
|
||||||
<CodeMirrorMerge
|
beforeMount={monacoCallback}
|
||||||
theme={light ? githubLight : githubDark}
|
options={{
|
||||||
orientation="a-b"
|
wordWrap: 'on',
|
||||||
>
|
minimap: { enabled: false },
|
||||||
<CodeMirrorMerge.Original
|
fontSize: 14,
|
||||||
readOnly
|
}}
|
||||||
value={data.currentAcl}
|
/>
|
||||||
extensions={[aclType]}
|
)
|
||||||
/>
|
: (
|
||||||
<CodeMirrorMerge.Modified
|
<DiffEditor
|
||||||
readOnly
|
height="100%"
|
||||||
value={acl}
|
language={language}
|
||||||
extensions={[aclType]}
|
theme={light ? 'light' : 'vs-dark'}
|
||||||
/>
|
original={original}
|
||||||
</CodeMirrorMerge>
|
modified={value}
|
||||||
)
|
loading={<Fallback acl={value} />}
|
||||||
)}
|
beforeMount={monacoCallback}
|
||||||
|
options={{
|
||||||
|
wordWrap: 'on',
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="heavy"
|
|
||||||
className="mr-2"
|
|
||||||
isDisabled={fetcher.state === 'loading' || !data.hasAclWrite || data.currentAcl === acl}
|
|
||||||
onPress={() => {
|
|
||||||
fetcher.submit({
|
|
||||||
acl,
|
|
||||||
}, {
|
|
||||||
method: 'PATCH',
|
|
||||||
encType: 'application/json',
|
|
||||||
})
|
|
||||||
|
|
||||||
toast('Updated tailnet ACL policy')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fetcher.state === 'idle'
|
|
||||||
? undefined
|
|
||||||
: (
|
|
||||||
<Spinner className="w-3 h-3" />
|
|
||||||
)}
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isDisabled={fetcher.state === 'loading' || data.currentAcl === acl}
|
|
||||||
onPress={() => {
|
|
||||||
setAcl(data.currentAcl)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Discard Changes
|
|
||||||
</Button>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,24 @@
|
|||||||
import clsx from 'clsx'
|
import Spinner from '~/components/Spinner'
|
||||||
|
import { cn } from '~/utils/cn'
|
||||||
|
|
||||||
import Button from '~/components/Button'
|
interface FallbackProps {
|
||||||
|
readonly acl: string
|
||||||
type FallbackProperties = {
|
|
||||||
readonly acl: string;
|
|
||||||
readonly where: 'client' | 'server';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Fallback({ acl, where }: FallbackProperties) {
|
export default function Fallback({ acl }: FallbackProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="inline-block relative w-full h-editor">
|
||||||
<div className={clsx(
|
<Spinner className="w-4 h-4 absolute p-2" />
|
||||||
where === 'server' ? 'mb-2 overflow-hidden rounded-tr-lg rounded-b-lg' : '',
|
<textarea
|
||||||
where === 'server' ? 'border border-gray-200 dark:border-gray-700' : ''
|
readOnly
|
||||||
)}
|
className={cn(
|
||||||
>
|
'w-full h-editor font-mono resize-none',
|
||||||
<textarea
|
'text-sm text-gray-600 dark:text-gray-300',
|
||||||
readOnly
|
'bg-ui-100 dark:bg-ui-800',
|
||||||
className={clsx(
|
'pl-16 pr-8 pt-0.5 leading-snug',
|
||||||
'w-full h-editor font-mono resize-none',
|
)}
|
||||||
'text-sm text-gray-600 dark:text-gray-300',
|
value={acl}
|
||||||
'pl-10 pt-1 leading-snug'
|
/>
|
||||||
)}
|
</div>
|
||||||
value={acl}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{where === 'server' ? (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant='heavy'
|
|
||||||
className='mr-2'
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button>
|
|
||||||
Discard Changes
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
) : undefined}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { BeakerIcon, EyeIcon, IssueDraftIcon, PencilIcon } from '@primer/octicons-react'
|
import { BeakerIcon, EyeIcon, IssueDraftIcon, PencilIcon } from '@primer/octicons-react'
|
||||||
import { type ActionFunctionArgs, json } from '@remix-run/node'
|
import { type ActionFunctionArgs, json } from '@remix-run/node'
|
||||||
import { useLoaderData } from '@remix-run/react'
|
import { useFetcher, useLoaderData } from '@remix-run/react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'
|
import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'
|
||||||
import { ClientOnly } from 'remix-utils/client-only'
|
|
||||||
|
|
||||||
|
import Button from '~/components/Button'
|
||||||
import Link from '~/components/Link'
|
import Link from '~/components/Link'
|
||||||
import Notice from '~/components/Notice'
|
import Notice from '~/components/Notice'
|
||||||
|
import Spinner from '~/components/Spinner'
|
||||||
|
import { toast } from '~/components/Toaster'
|
||||||
import { cn } from '~/utils/cn'
|
import { cn } from '~/utils/cn'
|
||||||
import { loadAcl, loadContext, patchAcl } from '~/utils/config/headplane'
|
import { loadAcl, loadContext, patchAcl } from '~/utils/config/headplane'
|
||||||
import { getSession } from '~/utils/sessions'
|
import { getSession } from '~/utils/sessions'
|
||||||
|
|
||||||
import Editor from './editor'
|
import Monaco from './editor'
|
||||||
import Fallback from './fallback'
|
|
||||||
|
|
||||||
export async function loader() {
|
export async function loader() {
|
||||||
const context = await loadContext()
|
const context = await loadContext()
|
||||||
@@ -56,6 +57,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
export default function Page() {
|
export default function Page() {
|
||||||
const data = useLoaderData<typeof loader>()
|
const data = useLoaderData<typeof loader>()
|
||||||
const [acl, setAcl] = useState(data.currentAcl)
|
const [acl, setAcl] = useState(data.currentAcl)
|
||||||
|
const fetcher = useFetcher()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -141,18 +143,21 @@ export default function Page() {
|
|||||||
</Tab>
|
</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel id="edit">
|
<TabPanel id="edit">
|
||||||
<ClientOnly fallback={<Fallback acl={acl} where="server" />}>
|
<Monaco
|
||||||
{() => (
|
variant="editor"
|
||||||
<Editor data={data} acl={acl} setAcl={setAcl} mode="edit" />
|
language={data.aclType}
|
||||||
)}
|
value={acl}
|
||||||
</ClientOnly>
|
onChange={setAcl}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="diff">
|
<TabPanel id="diff">
|
||||||
<ClientOnly fallback={<Fallback acl={acl} where="server" />}>
|
<Monaco
|
||||||
{() => (
|
variant="editor"
|
||||||
<Editor data={data} acl={acl} setAcl={setAcl} mode="diff" />
|
language={data.aclType}
|
||||||
)}
|
value={acl}
|
||||||
</ClientOnly>
|
onChange={setAcl}
|
||||||
|
original={data.currentAcl}
|
||||||
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="preview">
|
<TabPanel id="preview">
|
||||||
<div
|
<div
|
||||||
@@ -170,6 +175,31 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
<Button
|
||||||
|
variant="heavy"
|
||||||
|
className="mr-2"
|
||||||
|
isDisabled={fetcher.state === 'loading' || !data.hasAclWrite || data.currentAcl === acl}
|
||||||
|
onPress={() => {
|
||||||
|
fetcher.submit({
|
||||||
|
acl,
|
||||||
|
}, {
|
||||||
|
method: 'PATCH',
|
||||||
|
encType: 'application/json',
|
||||||
|
})
|
||||||
|
|
||||||
|
toast('Updated tailnet ACL policy')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fetcher.state === 'idle'
|
||||||
|
? undefined
|
||||||
|
: (
|
||||||
|
<Spinner className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
Discard Changes
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export async function loadContext(): Promise<HeadplaneContext> {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadAcl() {
|
export async function loadAcl(): Promise<{ data: string, type: 'json' | 'yaml' }> {
|
||||||
let path = process.env.ACL_FILE
|
let path = process.env.ACL_FILE
|
||||||
if (!path) {
|
if (!path) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+1
-5
@@ -11,26 +11,22 @@
|
|||||||
"typecheck": "tsc"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/modifiers": "^7.0.0",
|
"@dnd-kit/modifiers": "^7.0.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@primer/octicons-react": "^19.9.0",
|
"@primer/octicons-react": "^19.9.0",
|
||||||
"@react-aria/toast": "3.0.0-beta.11",
|
"@react-aria/toast": "3.0.0-beta.11",
|
||||||
"@react-stately/toast": "3.0.0-beta.3",
|
"@react-stately/toast": "3.0.0-beta.3",
|
||||||
"@remix-run/node": "^2.9.2",
|
"@remix-run/node": "^2.9.2",
|
||||||
"@remix-run/react": "^2.9.2",
|
"@remix-run/react": "^2.9.2",
|
||||||
"@remix-run/serve": "^2.9.2",
|
"@remix-run/serve": "^2.9.2",
|
||||||
"@uiw/codemirror-theme-github": "^4.22.0",
|
|
||||||
"@uiw/react-codemirror": "^4.22.0",
|
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"isbot": "^5.1.6",
|
"isbot": "^5.1.6",
|
||||||
"oauth4webapi": "^2.10.4",
|
"oauth4webapi": "^2.10.4",
|
||||||
"react": "19.0.0-beta-26f2496093-20240514",
|
"react": "19.0.0-beta-26f2496093-20240514",
|
||||||
"react-aria-components": "^1.2.0",
|
"react-aria-components": "^1.2.0",
|
||||||
"react-codemirror-merge": "^4.22.0",
|
|
||||||
"react-dom": "19.0.0-beta-26f2496093-20240514",
|
"react-dom": "19.0.0-beta-26f2496093-20240514",
|
||||||
"remix-utils": "^7.6.0",
|
"remix-utils": "^7.6.0",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
|
|||||||
Generated
+160
-403
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user