diff --git a/dashboard/src/components/templates/app/AppDeployInfo.tsx b/dashboard/src/components/templates/app/AppDeployInfo.tsx index b7919bfe..68dec3b1 100644 --- a/dashboard/src/components/templates/app/AppDeployInfo.tsx +++ b/dashboard/src/components/templates/app/AppDeployInfo.tsx @@ -80,6 +80,8 @@ const AppDeployInfo: Component<{ 'bg-color-overlay-ui-primary-to-transparency-primary-selected hover:bg-color-overlay-ui-primary-to-transparency-primary-hover', deploymentState(props.app) === ApplicationState.Idle && 'bg-color-overlay-ui-primary-to-black-alpha-200 hover:bg-color-overlay-ui-primary-to-black-alpha-100', + deploymentState(props.app) === ApplicationState.Sleeping && + 'bg-color-overlay-ui-primary-to-violet-200 hover:bg-color-overlay-ui-primary-to-violet-100', deploymentState(props.app) === ApplicationState.Deploying && 'bg-color-overlay-ui-primary-to-transparency-warn-selected hover:bg-color-overlay-ui-primary-to-transparency-warn-hover', deploymentState(props.app) === ApplicationState.Error && @@ -158,6 +160,9 @@ const AppDeployInfo: Component<{ 現在アプリが起動していないためSSHアクセスはできません + + アプリのURLにアクセスがあった場合、自動的に起動します + diff --git a/dashboard/src/components/templates/app/AppStatusIcon.tsx b/dashboard/src/components/templates/app/AppStatusIcon.tsx index 1c7e580f..a8e5d1b2 100644 --- a/dashboard/src/components/templates/app/AppStatusIcon.tsx +++ b/dashboard/src/components/templates/app/AppStatusIcon.tsx @@ -19,6 +19,9 @@ const components: Record JSXElement> = { style={{ 'font-size': `${props.size}px` }} /> ), + [ApplicationState.Sleeping]: (props) => ( +
+ ), [ApplicationState.Running]: (props) => (
origin: RepositoryOrigin[] @@ -31,6 +39,21 @@ const AppsFilter: Component<{ const filtered = () => props.statuses.length !== allStatuses.length || props.origin.length !== allOrigins.length || props.includeNoApp + const appCountByStatus = (status: ApplicationState): number => { + return props.allRepoWithApps + .filter((repo) => props.origin.includes(repositoryURLToOrigin(repo.repo.url))) + .flatMap((repo) => repo.apps.filter((app) => applicationState(app) === status)).length + } + + const repoCountByOrigin = (origin: RepositoryOrigin): number => { + return props.allRepoWithApps + .filter((repo) => repositoryURLToOrigin(repo.repo.url) === origin) + .filter( + (repo) => + props.includeNoApp || repo.apps.filter((app) => props.statuses.includes(applicationState(app))).length > 0, + ).length + } + return ( - Status + App Status {(s) => ( @@ -84,7 +107,9 @@ const AppsFilter: Component<{ - {s.label} + + {s.label} ({appCountByStatus(s.value)}) + )} @@ -92,7 +117,7 @@ const AppsFilter: Component<{ - Origin + Repo Origin {(s) => ( @@ -112,7 +137,9 @@ const AppsFilter: Component<{ {originToIcon(s.value)} - {s.label} + + {s.label} ({repoCountByOrigin(s.value)}) + )} diff --git a/dashboard/src/libs/application.tsx b/dashboard/src/libs/application.tsx index 82636e6e..6dca55e2 100644 --- a/dashboard/src/libs/application.tsx +++ b/dashboard/src/libs/application.tsx @@ -1,3 +1,4 @@ +import { timestampDate } from '@bufbuild/protobuf/wkt' import { AiFillGithub } from 'solid-icons/ai' import { RiDevelopmentGitRepositoryLine } from 'solid-icons/ri' import { SiGitea } from 'solid-icons/si' @@ -9,6 +10,7 @@ import { type CreateWebsiteRequest, DeployType, PortPublicationProtocol, + type Repository, type Website, } from '/@/api/neoshowcase/protobuf/gateway_pb' @@ -25,6 +27,7 @@ export enum ApplicationState { Idle = 'Idle', Deploying = 'Deploying', Running = 'Running', + Sleeping = 'Sleeping', Serving = 'Serving', Error = 'Error', } @@ -40,8 +43,8 @@ const autoShutdownEnabled = (app: Application): boolean => { } export const deploymentState = (app: Application): ApplicationState => { - // if app is not running or autoShutdown is enabled and container is missing, it's idle - if (!app.running || (autoShutdownEnabled(app) && app.container === Application_ContainerState.MISSING)) { + // App is not running + if (!app.running) { return ApplicationState.Idle } if (app.currentBuild === '') { @@ -51,6 +54,11 @@ export const deploymentState = (app: Application): ApplicationState => { if (app.deployType === DeployType.RUNTIME) { switch (app.container) { case Application_ContainerState.MISSING: + // Has auto shutdown enabled, and the container is missing - app is sleeping, and will start on HTTP access + if (autoShutdownEnabled(app)) { + return ApplicationState.Sleeping + } + return ApplicationState.Deploying case Application_ContainerState.STARTING: return ApplicationState.Deploying case Application_ContainerState.RUNNING: @@ -141,3 +149,53 @@ export const portPublicationProtocolMap: Record [PortPublicationProtocol.TCP]: 'TCP', [PortPublicationProtocol.UDP]: 'UDP', } + +const newestAppDate = (apps: Application[]): number => + Math.max(0, ...apps.map((a) => (a.updatedAt ? timestampDate(a.updatedAt).getTime() : 0))) +const compareRepoWithApp = + (sort: 'asc' | 'desc') => + (a: RepoWithApp, b: RepoWithApp): number => { + // Sort by apps updated at + if (a.apps.length > 0 && b.apps.length > 0) { + if (sort === 'asc') { + return newestAppDate(a.apps) - newestAppDate(b.apps) + } + return newestAppDate(b.apps) - newestAppDate(a.apps) + } + // Bring up repositories with 1 or more apps at top + if ((a.apps.length > 0 && b.apps.length === 0) || (a.apps.length === 0 && b.apps.length > 0)) { + return b.apps.length - a.apps.length + } + // Fallback to sort by repository id + return a.repo.id.localeCompare(b.repo.id) + } + +export interface RepoWithApp { + repo: Repository + apps: Application[] +} + +export const useApplicationsFilter = ( + repos: Repository[], + apps: Application[], + statuses: ApplicationState[], + origins: RepositoryOrigin[], + includeNoApp: boolean, + sort: 'asc' | 'desc', +): RepoWithApp[] => { + const filteredReposByOrigin = repos.filter((r) => origins.includes(repositoryURLToOrigin(r.url))) + const filteredApps = apps.filter((a) => statuses.includes(applicationState(a))) + + const appsMap = {} as Record + for (const app of filteredApps) { + if (!appsMap[app.repositoryId]) appsMap[app.repositoryId] = [] + appsMap[app.repositoryId].push(app) + } + const res = filteredReposByOrigin.reduce((acc, repo) => { + if (!includeNoApp && !appsMap[repo.id]) return acc + acc.push({ repo, apps: appsMap[repo.id] || [] }) + return acc + }, []) + res.sort(compareRepoWithApp(sort)) + return res +} diff --git a/dashboard/src/pages/apps.tsx b/dashboard/src/pages/apps.tsx index 87442aca..f36cc6f1 100644 --- a/dashboard/src/pages/apps.tsx +++ b/dashboard/src/pages/apps.tsx @@ -1,19 +1,13 @@ -import { timestampDate } from '@bufbuild/protobuf/wkt' import { Title } from '@solidjs/meta' import { A } from '@solidjs/router' import { createVirtualizer } from '@tanstack/solid-virtual' import Fuse from 'fuse.js' import { type Component, For, Suspense, createMemo, createResource, createSignal, useTransition } from 'solid-js' -import { - type Application, - GetApplicationsRequest_Scope, - GetRepositoriesRequest_Scope, - type Repository, -} from '/@/api/neoshowcase/protobuf/gateway_pb' +import { GetApplicationsRequest_Scope, GetRepositoriesRequest_Scope } from '/@/api/neoshowcase/protobuf/gateway_pb' import { styled } from '/@/components/styled-components' import type { SelectOption } from '/@/components/templates/Select' import { client, getRepositoryCommits, user } from '/@/libs/api' -import { ApplicationState, type RepositoryOrigin, applicationState, repositoryURLToOrigin } from '/@/libs/application' +import { ApplicationState, type RepoWithApp, type RepositoryOrigin, useApplicationsFilter } from '/@/libs/application' import { createSessionSignal } from '/@/libs/localStore' import { Button } from '../components/UI/Button' import { TabRound } from '../components/UI/TabRound' @@ -44,36 +38,12 @@ const scopeItems = (admin: boolean | undefined) => { } return items } -interface RepoWithApp { - repo: Repository - apps: Application[] -} - -const newestAppDate = (apps: Application[]): number => - Math.max(0, ...apps.map((a) => (a.updatedAt ? timestampDate(a.updatedAt).getTime() : 0))) -const compareRepoWithApp = - (sort: 'asc' | 'desc') => - (a: RepoWithApp, b: RepoWithApp): number => { - // Sort by apps updated at - if (a.apps.length > 0 && b.apps.length > 0) { - if (sort === 'asc') { - return newestAppDate(a.apps) - newestAppDate(b.apps) - } - return newestAppDate(b.apps) - newestAppDate(a.apps) - } - // Bring up repositories with 1 or more apps at top - if ((a.apps.length > 0 && b.apps.length === 0) || (a.apps.length === 0 && b.apps.length > 0)) { - return b.apps.length - a.apps.length - } - // Fallback to sort by repository id - return a.repo.id.localeCompare(b.repo.id) - } export const allStatuses: SelectOption[] = [ { label: 'Idle', value: ApplicationState.Idle }, { label: 'Deploying', value: ApplicationState.Deploying }, { label: 'Running', value: ApplicationState.Running }, - { label: 'Sleeping', value: ApplicationState.Idle }, + { label: 'Sleeping', value: ApplicationState.Sleeping }, { label: 'Serving', value: ApplicationState.Serving }, { label: 'Error', value: ApplicationState.Error }, ] @@ -84,62 +54,23 @@ export const allOrigins: SelectOption[] = [ ] const AppsList: Component<{ - scope: GetRepositoriesRequest_Scope - statuses: ApplicationState[] - origins: RepositoryOrigin[] + repoWithApps: RepoWithApp[] query: string - sort: keyof typeof sortItems - includeNoApp: boolean parentRef: HTMLDivElement }> = (props) => { - const appScope = () => { - const mine = props.scope === GetRepositoriesRequest_Scope.MINE - return mine ? GetApplicationsRequest_Scope.MINE : GetApplicationsRequest_Scope.ALL - } - const [repos] = createResource( - () => props.scope, - (scope) => client.getRepositories({ scope }), - ) - const [apps] = createResource( - () => appScope(), - (scope) => client.getApplications({ scope }), - ) - const hashes = () => apps()?.applications?.map((app) => app.commit) + const hashes = () => props.repoWithApps.flatMap((r) => r.apps.map((a) => a.commit)) const [commits] = createResource( () => hashes(), (hashes) => getRepositoryCommits(hashes), ) - const filteredReposByOrigin = createMemo(() => { - const p = props.origins - return repos()?.repositories.filter((r) => p.includes(repositoryURLToOrigin(r.url))) ?? [] - }) - const filteredApps = createMemo(() => { - const s = props.statuses - return apps()?.applications.filter((a) => s.includes(applicationState(a))) ?? [] - }) - const repoWithApps = createMemo(() => { - const appsMap = {} as Record - for (const app of filteredApps()) { - if (!appsMap[app.repositoryId]) appsMap[app.repositoryId] = [] - appsMap[app.repositoryId].push(app) - } - const res = filteredReposByOrigin().reduce((acc, repo) => { - if (!props.includeNoApp && !appsMap[repo.id]) return acc - acc.push({ repo, apps: appsMap[repo.id] || [] }) - return acc - }, []) - res.sort(compareRepoWithApp(props.sort)) - return res - }) - const fuse = createMemo(() => { - return new Fuse(repoWithApps(), { + return new Fuse(props.repoWithApps, { keys: ['repo.name', 'apps.name'], }) }) const filteredRepos = createMemo(() => { - if (props.query === '') return repoWithApps() + if (props.query === '') return props.repoWithApps return fuse() .search(props.query) .map((r) => r.item) @@ -215,17 +146,47 @@ export default () => { 'apps-statuses-v1', allStatuses.map((s) => s.value), ) - const [origin, setOrigin] = createSessionSignal('apps-repository-origin', [ - 'GitHub', - 'Gitea', - 'Others', - ]) + const [origin, setOrigin] = createSessionSignal( + 'apps-repository-origin', + allOrigins.map((o) => o.value), + ) const [query, setQuery] = createSessionSignal('apps-query', '') const [sort, setSort] = createSessionSignal('apps-sort', sortItems.desc.value) const [includeNoApp, setIncludeNoApp] = createSessionSignal('apps-include-no-app', false) const [scrollParentRef, setScrollParentRef] = createSignal() + const appScope = () => { + const mine = scope() === GetRepositoriesRequest_Scope.MINE + return mine ? GetApplicationsRequest_Scope.MINE : GetApplicationsRequest_Scope.ALL + } + const [repos] = createResource( + () => scope(), + (scope) => client.getRepositories({ scope }), + ) + const [apps] = createResource( + () => appScope(), + (scope) => client.getApplications({ scope }), + ) + const allRepoWithApps = () => + useApplicationsFilter( + repos()?.repositories ?? [], + apps()?.applications ?? [], + allStatuses.map((s) => s.value), + allOrigins.map((o) => o.value), + true, + 'desc', + ) + const repoWithApps = () => + useApplicationsFilter( + repos()?.repositories ?? [], + apps()?.applications ?? [], + statuses(), + origin(), + includeNoApp(), + sort(), + ) + return (
@@ -262,6 +223,7 @@ export default () => { leftIcon={
} rightIcon={ { } > - +