Add tabs to separate apps and tool repos
This commit is contained in:
parent
946cdb1794
commit
267e4e59f7
|
|
@ -15,26 +15,6 @@ const TOES_URL = process.env.TOES_URL!
|
||||||
const REPOS_DIR = resolve(DATA_ROOT, 'repos')
|
const REPOS_DIR = resolve(DATA_ROOT, 'repos')
|
||||||
const VISIBILITY_PATH = join(DATA_DIR, 'visibility.json')
|
const VISIBILITY_PATH = join(DATA_DIR, 'visibility.json')
|
||||||
|
|
||||||
const TOGGLE_SCRIPT = `
|
|
||||||
function toggleVisibility(btn) {
|
|
||||||
var repo = btn.dataset.repo;
|
|
||||||
var current = btn.dataset.visibility;
|
|
||||||
var next = current === 'public' ? 'private' : 'public';
|
|
||||||
btn.dataset.visibility = next;
|
|
||||||
btn.textContent = next;
|
|
||||||
btn.classList.toggle('public', next === 'public');
|
|
||||||
fetch('/api/visibility/' + encodeURIComponent(repo), {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ visibility: next })
|
|
||||||
}).catch(function() {
|
|
||||||
btn.dataset.visibility = current;
|
|
||||||
btn.textContent = current;
|
|
||||||
btn.classList.toggle('public', current === 'public');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const app = new Hype({ prettyHTML: false, layout: false })
|
const app = new Hype({ prettyHTML: false, layout: false })
|
||||||
const deployLocks = new Map<string, Promise<void>>()
|
const deployLocks = new Map<string, Promise<void>>()
|
||||||
|
|
||||||
|
|
@ -113,6 +93,31 @@ const RepoName = define('RepoName', {
|
||||||
color: theme('colors-text'),
|
color: theme('colors-text'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const Tab = define('Tab', {
|
||||||
|
base: 'button',
|
||||||
|
padding: '6px 0',
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
borderBottom: '2px solid transparent',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: theme('colors-textMuted'),
|
||||||
|
states: {
|
||||||
|
':hover': { color: theme('colors-text') },
|
||||||
|
'.active': {
|
||||||
|
color: theme('colors-text'),
|
||||||
|
borderBottomColor: theme('colors-primary'),
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const TabBar = define('TabBar', {
|
||||||
|
display: 'flex',
|
||||||
|
gap: '24px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
})
|
||||||
|
|
||||||
const Toggle = define('Toggle', {
|
const Toggle = define('Toggle', {
|
||||||
base: 'button',
|
base: 'button',
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
|
|
@ -156,7 +161,7 @@ interface LayoutProps {
|
||||||
interface RepoListPageProps {
|
interface RepoListPageProps {
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
external: boolean
|
external: boolean
|
||||||
repos: Array<{ name: string; commits: boolean; branch: string; visibility: Visibility }>
|
repos: Array<{ name: string; commits: boolean; branch: string; visibility: Visibility; tool: boolean }>
|
||||||
tunnelUrl?: string
|
tunnelUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -544,7 +549,56 @@ function AppRepo({ appName, baseUrl, branch, exists, commits }: AppRepoProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RepoListItems({ baseUrl, external, repos, tunnelUrl }: {
|
||||||
|
baseUrl: string
|
||||||
|
external: boolean
|
||||||
|
repos: RepoListPageProps['repos']
|
||||||
|
tunnelUrl?: string
|
||||||
|
}) {
|
||||||
|
if (repos.length === 0) {
|
||||||
|
return <HelpText>No repositories yet.</HelpText>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<RepoList>
|
||||||
|
{repos.map(({ name, commits, branch, visibility }) => (
|
||||||
|
<RepoItem>
|
||||||
|
<div>
|
||||||
|
<RepoName>{name}</RepoName>
|
||||||
|
<HelpText style="margin: 4px 0 0; font-size: 12px">
|
||||||
|
git clone {baseUrl}/{name}
|
||||||
|
</HelpText>
|
||||||
|
{!external && tunnelUrl && visibility === 'public' && (
|
||||||
|
<HelpText style="margin: 2px 0 0; font-size: 12px">
|
||||||
|
git clone {tunnelUrl}/{name}
|
||||||
|
</HelpText>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; align-items: center">
|
||||||
|
{!external && (
|
||||||
|
<Toggle
|
||||||
|
class={visibility === 'public' ? 'public' : ''}
|
||||||
|
data-repo={name}
|
||||||
|
data-visibility={visibility}
|
||||||
|
onclick="toggleVisibility(this)"
|
||||||
|
>
|
||||||
|
{visibility === 'public' ? 'public' : 'private'}
|
||||||
|
</Toggle>
|
||||||
|
)}
|
||||||
|
<Badge>{branch}</Badge>
|
||||||
|
{commits
|
||||||
|
? <Badge style={`color: ${theme('colors-statusRunning')}`}>deployed</Badge>
|
||||||
|
: <Badge>empty</Badge>}
|
||||||
|
</div>
|
||||||
|
</RepoItem>
|
||||||
|
))}
|
||||||
|
</RepoList>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function RepoListPage({ baseUrl, external, repos, tunnelUrl }: RepoListPageProps) {
|
function RepoListPage({ baseUrl, external, repos, tunnelUrl }: RepoListPageProps) {
|
||||||
|
const appRepos = repos.filter(r => !r.tool)
|
||||||
|
const toolRepos = repos.filter(r => r.tool)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout title="Git">
|
<Layout title="Git">
|
||||||
{!external && (
|
{!external && (
|
||||||
|
|
@ -569,40 +623,20 @@ function RepoListPage({ baseUrl, external, repos, tunnelUrl }: RepoListPageProps
|
||||||
{repos.length > 0 && (
|
{repos.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Heading>Repositories</Heading>
|
<Heading>Repositories</Heading>
|
||||||
<RepoList>
|
<TabBar>
|
||||||
{repos.map(({ name, commits, branch, visibility }) => (
|
<Tab class="active" data-tab="tab-apps" onclick="switchTab(this)">Apps</Tab>
|
||||||
<RepoItem>
|
<Tab data-tab="tab-tools" onclick="switchTab(this)">Tools</Tab>
|
||||||
<div>
|
</TabBar>
|
||||||
<RepoName>{name}</RepoName>
|
<div>
|
||||||
<HelpText style="margin: 4px 0 0; font-size: 12px">
|
<div id="tab-apps">
|
||||||
git clone {baseUrl}/{name}
|
<RepoListItems baseUrl={baseUrl} external={external} repos={appRepos} tunnelUrl={tunnelUrl} />
|
||||||
</HelpText>
|
</div>
|
||||||
{!external && tunnelUrl && visibility === 'public' && (
|
<div id="tab-tools" style="display: none">
|
||||||
<HelpText style="margin: 2px 0 0; font-size: 12px">
|
<RepoListItems baseUrl={baseUrl} external={external} repos={toolRepos} tunnelUrl={tunnelUrl} />
|
||||||
git clone {tunnelUrl}/{name}
|
</div>
|
||||||
</HelpText>
|
</div>
|
||||||
)}
|
{!external && <script src="/client/toggle.js" />}
|
||||||
</div>
|
<script src="/client/tabs.js" />
|
||||||
<div style="display: flex; gap: 8px; align-items: center">
|
|
||||||
{!external && (
|
|
||||||
<Toggle
|
|
||||||
class={visibility === 'public' ? 'public' : ''}
|
|
||||||
data-repo={name}
|
|
||||||
data-visibility={visibility}
|
|
||||||
onclick="toggleVisibility(this)"
|
|
||||||
>
|
|
||||||
{visibility === 'public' ? 'public' : 'private'}
|
|
||||||
</Toggle>
|
|
||||||
)}
|
|
||||||
<Badge>{branch}</Badge>
|
|
||||||
{commits
|
|
||||||
? <Badge style={`color: ${theme('colors-statusRunning')}`}>deployed</Badge>
|
|
||||||
: <Badge>empty</Badge>}
|
|
||||||
</div>
|
|
||||||
</RepoItem>
|
|
||||||
))}
|
|
||||||
</RepoList>
|
|
||||||
{!external && <script src="/toggle.js" />}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -630,9 +664,6 @@ app.get('/styles.css', c =>
|
||||||
c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' }),
|
c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' }),
|
||||||
)
|
)
|
||||||
|
|
||||||
app.get('/toggle.js', c =>
|
|
||||||
c.text(TOGGLE_SCRIPT, 200, { 'Content-Type': 'application/javascript; charset=utf-8' }),
|
|
||||||
)
|
|
||||||
|
|
||||||
// GET /:repo[.git]/info/refs?service=git-upload-pack|git-receive-pack
|
// GET /:repo[.git]/info/refs?service=git-upload-pack|git-receive-pack
|
||||||
app.on('GET', ['/:repo{.+\\.git}/info/refs', '/:repo/info/refs'], async c => {
|
app.on('GET', ['/:repo{.+\\.git}/info/refs', '/:repo/info/refs'], async c => {
|
||||||
|
|
@ -780,6 +811,19 @@ app.get('/', async c => {
|
||||||
|
|
||||||
// No app selected — show all repos
|
// No app selected — show all repos
|
||||||
const repos = await listRepos()
|
const repos = await listRepos()
|
||||||
|
|
||||||
|
// Fetch all apps to determine which repos are tools
|
||||||
|
let toolSet = new Set<string>()
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${TOES_URL}/api/apps`)
|
||||||
|
if (res.ok) {
|
||||||
|
const apps = await res.json() as Array<{ name: string; tool?: boolean | string }>
|
||||||
|
for (const a of apps) {
|
||||||
|
if (a.tool) toolSet.add(a.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
const repoData = await Promise.all(repos.map(async name => {
|
const repoData = await Promise.all(repos.map(async name => {
|
||||||
const bare = repoPath(name)
|
const bare = repoPath(name)
|
||||||
const [commits, branch, visibility] = await Promise.all([
|
const [commits, branch, visibility] = await Promise.all([
|
||||||
|
|
@ -787,7 +831,7 @@ app.get('/', async c => {
|
||||||
getDefaultBranch(bare),
|
getDefaultBranch(bare),
|
||||||
getVisibility(name),
|
getVisibility(name),
|
||||||
])
|
])
|
||||||
return { name, commits, branch, visibility }
|
return { name, commits, branch, visibility, tool: toolSet.has(name) }
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Hide private repos from external (sneaker) requests
|
// Hide private repos from external (sneaker) requests
|
||||||
|
|
|
||||||
13
apps/git/src/client/tabs.ts
Normal file
13
apps/git/src/client/tabs.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
function switchTab(btn: HTMLButtonElement) {
|
||||||
|
const tabs = btn.parentElement!.querySelectorAll('button')
|
||||||
|
for (const tab of tabs) tab.classList.remove('active')
|
||||||
|
btn.classList.add('active')
|
||||||
|
|
||||||
|
const panels = btn.parentElement!.nextElementSibling!.children
|
||||||
|
for (const panel of panels) (panel as HTMLElement).style.display = 'none'
|
||||||
|
|
||||||
|
const target = document.getElementById(btn.dataset.tab!)
|
||||||
|
if (target) target.style.display = 'block'
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(window, { switchTab })
|
||||||
19
apps/git/src/client/toggle.ts
Normal file
19
apps/git/src/client/toggle.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
function toggleVisibility(btn: HTMLButtonElement) {
|
||||||
|
const repo = btn.dataset.repo!
|
||||||
|
const current = btn.dataset.visibility!
|
||||||
|
const next = current === 'public' ? 'private' : 'public'
|
||||||
|
btn.dataset.visibility = next
|
||||||
|
btn.textContent = next
|
||||||
|
btn.classList.toggle('public', next === 'public')
|
||||||
|
fetch('/api/visibility/' + encodeURIComponent(repo), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ visibility: next }),
|
||||||
|
}).catch(() => {
|
||||||
|
btn.dataset.visibility = current
|
||||||
|
btn.textContent = current
|
||||||
|
btn.classList.toggle('public', current === 'public')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(window, { toggleVisibility })
|
||||||
Loading…
Reference in New Issue
Block a user