Compare commits

..

9 Commits

Author SHA1 Message Date
Aarnav Tale 74e87bd80e chore: v0.1.9 2024-06-03 22:52:26 -04:00
Aarnav Tale d01699009d chore: update packages 2024-06-03 22:51:45 -04:00
Aarnav Tale 08db192db1 fix: use magic domain for machine domain attribute 2024-06-03 22:43:38 -04:00
Aarnav Tale 3ea5fed8f6 fix: support jsonc comments and switch to monaco for acls 2024-06-02 22:40:23 -04:00
Aarnav Tale dd9d6cd550 fix: set edge tag to main 2024-06-02 16:52:29 -04:00
Aarnav Tale f82244f85a fix: github actions work this time? 2024-06-02 14:33:43 -04:00
Aarnav Tale 896aaad61e fix: push action had syntax error 2024-06-02 14:29:32 -04:00
Aarnav Tale e5dece5b7b feat: add build and nightly 2024-06-02 14:28:42 -04:00
Aarnav Tale 2797525969 fix: expired check 2.0? 2024-06-02 14:03:25 -04:00
13 changed files with 1315 additions and 1485 deletions
+44
View File
@@ -0,0 +1,44 @@
name: Publish Nightly Docker Image
on:
schedule:
- cron: '0 8 * * *'
workflow_dispatch:
jobs:
publish:
name: Build and Publish Nightly
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker Metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=edge,branch=main
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64, linux/arm64
+44
View File
@@ -0,0 +1,44 @@
name: 'Build'
on:
push:
branches:
- 'main'
pull_request:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Install node.js
uses: actions/setup-node@v4
with:
node-version: 20
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
+6
View File
@@ -1,3 +1,9 @@
### 0.1.9 (June 2, 2024)
- Switch to Monaco editor with proper HuJSON and YAML syntax highlighting.
- Utilize magic DNS hostnames for the machine overview page.
- Fixed the expiry issue once and for all.
- Add a nightly build with the `ghcr.io/tale/headplane:edge` tag
### 0.1.8 (June 2, 2024)
- Built basic functionality for the machine overview page (by machine ID).
- Possibly fixed an issue where expiry disabled machines' timestamps weren't handled correctly.
+69 -97
View File
@@ -1,36 +1,33 @@
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, useMemo, useState } from 'react'
import CodeMirrorMerge from 'react-codemirror-merge'
import Editor, { DiffEditor, Monaco } from '@monaco-editor/react'
import { useEffect, useState } from 'react'
import { ClientOnly } from 'remix-utils/client-only'
import Button from '~/components/Button'
import Spinner from '~/components/Spinner'
import { toast } from '~/components/Toaster'
import Fallback from '~/routes/_data.acls._index/fallback'
import { cn } from '~/utils/cn'
import Fallback from './fallback'
interface EditorProperties {
readonly acl: string
readonly setAcl: (acl: string) => void
readonly mode: 'edit' | 'diff'
readonly data: {
hasAclWrite: boolean
currentAcl: string
aclType: string
}
interface MonacoProps {
variant: 'editor' | 'diff'
language: 'json' | 'yaml'
value: string
onChange: (value: string) => void
original?: string
}
export default function Editor({ data, acl, setAcl, mode }: EditorProperties) {
const [light, setLight] = useState(false)
const [loading, setLoading] = useState(true)
function monacoCallback(monaco: Monaco) {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
allowComments: true,
schemas: [],
enableSchemaRequest: true,
trailingCommas: 'ignore',
})
const fetcher = useFetcher()
const aclType = useMemo(() => data.aclType === 'json' ? json() : yaml(), [data.aclType])
monaco.languages.register({ id: 'json' })
monaco.languages.register({ id: 'yaml' })
}
export default function MonacoEditor({ value, onChange, variant, original, language }: MonacoProps) {
const [light, setLight] = useState(false)
useEffect(() => {
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) => {
setLight(theme.matches)
})
// Prevents the FOUC
setLoading(false)
}, [])
return (
<>
<div className={clsx(
<div className={cn(
'border border-gray-200 dark:border-gray-700',
'rounded-b-lg rounded-tr-lg mb-2 z-10 overflow-x-hidden',
)}
>
<div className="overflow-y-scroll h-editor text-sm">
{loading
? (
<Fallback acl={acl} where="client" />
)
: (
mode === 'edit'
? (
<CodeMirror
value={acl}
theme={light ? githubLight : githubDark}
extensions={[aclType]}
readOnly={!data.hasAclWrite}
onChange={(value) => {
setAcl(value)
}}
/>
)
: (
<CodeMirrorMerge
theme={light ? githubLight : githubDark}
orientation="a-b"
>
<CodeMirrorMerge.Original
readOnly
value={data.currentAcl}
extensions={[aclType]}
/>
<CodeMirrorMerge.Modified
readOnly
value={acl}
extensions={[aclType]}
/>
</CodeMirrorMerge>
)
)}
<ClientOnly fallback={<Fallback acl={value} />}>
{() => variant === 'editor'
? (
<Editor
height="100%"
language={language}
theme={light ? 'light' : 'vs-dark'}
value={value}
onChange={(updated) => {
if (!updated) {
return
}
if (updated !== value) {
onChange(updated)
}
}}
loading={<Fallback acl={value} />}
beforeMount={monacoCallback}
options={{
wordWrap: 'on',
minimap: { enabled: false },
fontSize: 14,
}}
/>
)
: (
<DiffEditor
height="100%"
language={language}
theme={light ? 'light' : 'vs-dark'}
original={original}
modified={value}
loading={<Fallback acl={value} />}
beforeMount={monacoCallback}
options={{
wordWrap: 'on',
minimap: { enabled: false },
fontSize: 13,
}}
/>
)}
</ClientOnly>
</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>
</>
)
}
+18 -37
View File
@@ -1,43 +1,24 @@
import clsx from 'clsx'
import Spinner from '~/components/Spinner'
import { cn } from '~/utils/cn'
import Button from '~/components/Button'
type FallbackProperties = {
readonly acl: string;
readonly where: 'client' | 'server';
interface FallbackProps {
readonly acl: string
}
export default function Fallback({ acl, where }: FallbackProperties) {
export default function Fallback({ acl }: FallbackProps) {
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
variant='heavy'
className='mr-2'
>
Save
</Button>
<Button>
Discard Changes
</Button>
</>
) : undefined}
</>
<div className="inline-block relative w-full h-editor">
<Spinner className="w-4 h-4 absolute p-2" />
<textarea
readOnly
className={cn(
'w-full h-editor font-mono resize-none',
'text-sm text-gray-600 dark:text-gray-300',
'bg-ui-100 dark:bg-ui-800',
'pl-16 pr-8 pt-0.5 leading-snug',
)}
value={acl}
/>
</div>
)
}
+44 -14
View File
@@ -1,18 +1,19 @@
import { BeakerIcon, EyeIcon, IssueDraftIcon, PencilIcon } from '@primer/octicons-react'
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 { 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 Notice from '~/components/Notice'
import Spinner from '~/components/Spinner'
import { toast } from '~/components/Toaster'
import { cn } from '~/utils/cn'
import { loadAcl, loadContext, patchAcl } from '~/utils/config/headplane'
import { getSession } from '~/utils/sessions'
import Editor from './editor'
import Fallback from './fallback'
import Monaco from './editor'
export async function loader() {
const context = await loadContext()
@@ -56,6 +57,7 @@ export async function action({ request }: ActionFunctionArgs) {
export default function Page() {
const data = useLoaderData<typeof loader>()
const [acl, setAcl] = useState(data.currentAcl)
const fetcher = useFetcher()
return (
<div>
@@ -141,18 +143,21 @@ export default function Page() {
</Tab>
</TabList>
<TabPanel id="edit">
<ClientOnly fallback={<Fallback acl={acl} where="server" />}>
{() => (
<Editor data={data} acl={acl} setAcl={setAcl} mode="edit" />
)}
</ClientOnly>
<Monaco
variant="editor"
language={data.aclType}
value={acl}
onChange={setAcl}
/>
</TabPanel>
<TabPanel id="diff">
<ClientOnly fallback={<Fallback acl={acl} where="server" />}>
{() => (
<Editor data={data} acl={acl} setAcl={setAcl} mode="diff" />
)}
</ClientOnly>
<Monaco
variant="editor"
language={data.aclType}
value={acl}
onChange={setAcl}
original={data.currentAcl}
/>
</TabPanel>
<TabPanel id="preview">
<div
@@ -170,6 +175,31 @@ export default function Page() {
</div>
</TabPanel>
</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>
)
}
+10 -5
View File
@@ -49,6 +49,7 @@ export default function Page() {
useLiveData({ interval: 1000 })
const expired = machine.expiry === '0001-01-01 00:00:00'
|| machine.expiry === '0001-01-01T00:00:00Z'
? false
: new Date(machine.expiry).getTime() < Date.now()
@@ -128,11 +129,15 @@ export default function Page() {
name="Expiry"
value={new Date(machine.expiry).toLocaleString()}
/>
<Attribute
isCopyable
name="Domain"
value={`${machine.givenName}.${machine.user.name}.ts.net`}
/>
{magic
? (
<Attribute
isCopyable
name="Domain"
value={`${machine.givenName}.${machine.user.name}.${magic}`}
/>
)
: undefined}
</Card>
<h2 className="text-xl font-medium mb-4 mt-8">
Machine Routes
@@ -17,6 +17,7 @@ interface Props {
export default function MachineRow({ machine, routes, magic }: Props) {
const expired = machine.expiry === '0001-01-01 00:00:00'
|| machine.expiry === '0001-01-01T00:00:00Z'
? false
: new Date(machine.expiry).getTime() < Date.now()
@@ -23,6 +23,7 @@ export default function Menu({ machine, routes, magic }: MenuProps) {
const routesState = useState(false)
const expired = machine.expiry === '0001-01-01 00:00:00'
|| machine.expiry === '0001-01-01T00:00:00Z'
? false
: new Date(machine.expiry).getTime() < Date.now()
+1 -1
View File
@@ -83,7 +83,7 @@ export async function loadContext(): Promise<HeadplaneContext> {
return context
}
export async function loadAcl() {
export async function loadAcl(): Promise<{ data: string, type: 'json' | 'yaml' }> {
let path = process.env.ACL_FILE
if (!path) {
try {
+10 -14
View File
@@ -11,33 +11,29 @@
"typecheck": "tsc"
},
"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",
"@dnd-kit/utilities": "^3.2.2",
"@monaco-editor/react": "^4.6.0",
"@primer/octicons-react": "^19.9.0",
"@react-aria/toast": "3.0.0-beta.11",
"@react-stately/toast": "3.0.0-beta.3",
"@react-aria/toast": "3.0.0-beta.12",
"@react-stately/toast": "3.0.0-beta.4",
"@remix-run/node": "^2.9.2",
"@remix-run/react": "^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",
"isbot": "^5.1.6",
"isbot": "^5.1.8",
"oauth4webapi": "^2.10.4",
"react": "19.0.0-beta-26f2496093-20240514",
"react-aria-components": "^1.2.0",
"react-codemirror-merge": "^4.22.0",
"react-aria-components": "^1.2.1",
"react-dom": "19.0.0-beta-26f2496093-20240514",
"remix-utils": "^7.6.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-react-aria-components": "^1.1.2",
"undici": "^6.16.1",
"tailwindcss-react-aria-components": "^1.1.3",
"undici": "^6.18.2",
"usehooks-ts": "^3.1.0",
"yaml": "^2.4.2",
"yaml": "^2.4.3",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -52,7 +48,7 @@
"tailwindcss": "^3.4.3",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"vite": "^5.2.12",
"vite-plugin-babel": "^1.2.0",
"vite-tsconfig-paths": "^4.2.1"
},
@@ -66,7 +62,7 @@
},
"pnpm": {
"patchedDependencies": {
"@react-aria/overlays@3.22.0": "patches/@react-aria__overlays@3.22.0.patch"
"@react-aria/overlays@3.22.1": "patches/@react-aria__overlays@3.22.1.patch"
}
}
}
@@ -1,13 +1,13 @@
diff --git a/dist/usePreventScroll.mjs b/dist/usePreventScroll.mjs
index 532a72f9e9ba985d036b7eba0f950ff6552898d9..7c9263667c2463c8e84fd07064076132fe53284c 100644
index 69b84ce2aec5b637a9f0ba8158b6a1ba4173c266..5e546a6e4fc3b7a0468c1c89d887a4392c010624 100644
--- a/dist/usePreventScroll.mjs
+++ b/dist/usePreventScroll.mjs
@@ -48,7 +48,7 @@ function $49c51c25361d4cd2$export$ee0f7cc6afcd1c18(options = {}) {
// For most browsers, all we need to do is set `overflow: hidden` on the root element, and
// add some padding to prevent the page from shifting when the scrollbar is hidden.
function $49c51c25361d4cd2$var$preventScrollStandard() {
- return (0, $7mMvr$chain)($49c51c25361d4cd2$var$setStyle(document.documentElement, "paddingRight", `${window.innerWidth - document.documentElement.clientWidth}px`), $49c51c25361d4cd2$var$setStyle(document.documentElement, "overflow", "hidden"));
+ return (0, $7mMvr$chain)($49c51c25361d4cd2$var$setStyle(document.documentElement, "overflow", "hidden"));
- return (0, $7mMvr$chain)($49c51c25361d4cd2$var$setStyle(document.documentElement, 'paddingRight', `${window.innerWidth - document.documentElement.clientWidth}px`), $49c51c25361d4cd2$var$setStyle(document.documentElement, 'overflow', 'hidden'));
+ return (0, $7mMvr$chain)($49c51c25361d4cd2$var$setStyle(document.documentElement, 'overflow', 'hidden'));
}
// Mobile Safari is a whole different beast. Even with overflow: hidden,
// it still scrolls the page in many situations:
@@ -15,8 +15,8 @@ index 532a72f9e9ba985d036b7eba0f950ff6552898d9..7c9263667c2463c8e84fd07064076132
// enable us to scroll the window to the top, which is required for the rest of this to work.
let scrollX = window.pageXOffset;
let scrollY = window.pageYOffset;
- restoreStyles = (0, $7mMvr$chain)($49c51c25361d4cd2$var$addEvent(window, "scroll", onWindowScroll), $49c51c25361d4cd2$var$setStyle(document.documentElement, "paddingRight", `${window.innerWidth - document.documentElement.clientWidth}px`), $49c51c25361d4cd2$var$setStyle(document.documentElement, "overflow", "hidden"), $49c51c25361d4cd2$var$setStyle(document.body, "marginTop", `-${scrollY}px`), ()=>{
+ restoreStyles = (0, $7mMvr$chain)($49c51c25361d4cd2$var$addEvent(window, "scroll", onWindowScroll), $49c51c25361d4cd2$var$setStyle(document.documentElement, "overflow", "hidden"), $49c51c25361d4cd2$var$setStyle(document.body, "marginTop", `-${scrollY}px`), ()=>{
- restoreStyles = (0, $7mMvr$chain)($49c51c25361d4cd2$var$addEvent(window, 'scroll', onWindowScroll), $49c51c25361d4cd2$var$setStyle(document.documentElement, 'paddingRight', `${window.innerWidth - document.documentElement.clientWidth}px`), $49c51c25361d4cd2$var$setStyle(document.documentElement, 'overflow', 'hidden'), $49c51c25361d4cd2$var$setStyle(document.body, 'marginTop', `-${scrollY}px`), ()=>{
+ restoreStyles = (0, $7mMvr$chain)($49c51c25361d4cd2$var$addEvent(window, 'scroll', onWindowScroll), $49c51c25361d4cd2$var$setStyle(document.documentElement, 'overflow', 'hidden'), $49c51c25361d4cd2$var$setStyle(document.body, 'marginTop', `-${scrollY}px`), ()=>{
window.scrollTo(scrollX, scrollY);
});
// Scroll to the top. The negative margin on the body will make this appear the same.
+1062 -1312
View File
File diff suppressed because it is too large Load Diff