From d323505e337f06cce7271e6ac679195ec1ee2703 Mon Sep 17 00:00:00 2001 From: motoki317 Date: Tue, 7 Jan 2025 18:05:58 +0900 Subject: [PATCH 1/6] feat: Show sleep status correctly --- .../src/components/templates/app/AppDeployInfo.tsx | 2 ++ .../src/components/templates/app/AppStatusIcon.tsx | 3 +++ dashboard/src/libs/application.tsx | 10 ++++++++-- dashboard/src/pages/apps.tsx | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/dashboard/src/components/templates/app/AppDeployInfo.tsx b/dashboard/src/components/templates/app/AppDeployInfo.tsx index b7919bfe..1c7af620 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 && 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) => (
{ } 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 +52,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: diff --git a/dashboard/src/pages/apps.tsx b/dashboard/src/pages/apps.tsx index 87442aca..35115ae8 100644 --- a/dashboard/src/pages/apps.tsx +++ b/dashboard/src/pages/apps.tsx @@ -73,7 +73,7 @@ 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 }, ] From c2ae0a3d19c79e678a7379a54fb9cd95530b9e25 Mon Sep 17 00:00:00 2001 From: motoki317 Date: Tue, 7 Jan 2025 18:54:16 +0900 Subject: [PATCH 2/6] refactor: extract app filter function from view --- dashboard/src/libs/application.tsx | 61 +++++++++++++++- dashboard/src/pages/apps.tsx | 113 ++++++++--------------------- 2 files changed, 89 insertions(+), 85 deletions(-) diff --git a/dashboard/src/libs/application.tsx b/dashboard/src/libs/application.tsx index ec361da4..87f8db16 100644 --- a/dashboard/src/libs/application.tsx +++ b/dashboard/src/libs/application.tsx @@ -1,16 +1,21 @@ import { AiFillGithub } from 'solid-icons/ai' import { RiDevelopmentGitRepositoryLine } from 'solid-icons/ri' import { SiGitea } from 'solid-icons/si' -import type { JSXElement } from 'solid-js' +import { createMemo, createResource, JSXElement } from 'solid-js' import { type Application, Application_ContainerState, BuildStatus, type CreateWebsiteRequest, DeployType, + GetApplicationsRequest_Scope, + GetRepositoriesRequest_Scope, PortPublicationProtocol, + type Repository, type Website, } from '/@/api/neoshowcase/protobuf/gateway_pb' +import { client, getRepositoryCommits } from '/@/libs/api' +import { timestampDate } from '@bufbuild/protobuf/wkt' export const buildStatusStr: Record = { [BuildStatus.QUEUED]: 'Queued', @@ -147,3 +152,57 @@ 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 = () => { + return repos.filter((r) => origins.includes(repositoryURLToOrigin(r.url))) ?? [] + } + const filteredApps = () => { + return 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 35115ae8..b7c3cc8b 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 RepositoryOrigin, useApplicationsFilter, RepoWithApp } from '/@/libs/application' import { createSessionSignal } from '/@/libs/localStore' import { Button } from '../components/UI/Button' import { TabRound } from '../components/UI/TabRound' @@ -44,30 +38,6 @@ 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 }, @@ -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) @@ -226,6 +157,28 @@ export default () => { 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 repoWithApps = () => + useApplicationsFilter( + repos()?.repositories ?? [], + apps()?.applications ?? [], + statuses(), + origin(), + includeNoApp(), + sort(), + ) + return (
@@ -285,15 +238,7 @@ export default () => { } > - +
From 1f422f8823b14ef669af41d45d88791adf5eef88 Mon Sep 17 00:00:00 2001 From: motoki317 Date: Tue, 7 Jan 2025 19:46:02 +0900 Subject: [PATCH 3/6] feat: Display app/repo count in filter --- .../components/templates/app/AppsFilter.tsx | 34 ++++++++++++++++--- dashboard/src/libs/application.tsx | 7 ++-- dashboard/src/pages/apps.tsx | 21 ++++++++---- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/dashboard/src/components/templates/app/AppsFilter.tsx b/dashboard/src/components/templates/app/AppsFilter.tsx index 3b9bc054..21681a01 100644 --- a/dashboard/src/components/templates/app/AppsFilter.tsx +++ b/dashboard/src/components/templates/app/AppsFilter.tsx @@ -3,7 +3,14 @@ import { type Component, type ComponentProps, For, type Setter, Show } from 'sol import { CheckBoxIcon } from '/@/components/UI/CheckBoxIcon' import { RadioIcon } from '/@/components/UI/RadioIcon' import { styled } from '/@/components/styled-components' -import { type ApplicationState, type RepositoryOrigin, originToIcon } from '/@/libs/application' +import { + type ApplicationState, + type RepoWithApp, + type RepositoryOrigin, + applicationState, + originToIcon, + repositoryURLToOrigin, +} from '/@/libs/application' import { clsx } from '/@/libs/clsx' import { allOrigins, allStatuses, sortItems } from '/@/pages/apps' import { AppStatusIcon } from './AppStatusIcon' @@ -19,6 +26,7 @@ const selectItemStyle = clsx( const FilterItemContainer = styled('div', 'flex flex-col gap-2 text-bold text-text-black') const AppsFilter: Component<{ + allRepoWithApps: RepoWithApp[] statuses: ApplicationState[] setStatues: Setter origin: RepositoryOrigin[] @@ -31,6 +39,18 @@ 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.length > 0).length + } + return ( - Status + App Status {(s) => ( @@ -84,7 +104,9 @@ const AppsFilter: Component<{ - {s.label} + + {s.label} ({appCountByStatus(s.value)}) + )} @@ -92,7 +114,7 @@ const AppsFilter: Component<{ - Origin + Repo Origin {(s) => ( @@ -112,7 +134,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 87f8db16..4425794d 100644 --- a/dashboard/src/libs/application.tsx +++ b/dashboard/src/libs/application.tsx @@ -1,21 +1,18 @@ +import { timestampDate } from '@bufbuild/protobuf/wkt' import { AiFillGithub } from 'solid-icons/ai' import { RiDevelopmentGitRepositoryLine } from 'solid-icons/ri' import { SiGitea } from 'solid-icons/si' -import { createMemo, createResource, JSXElement } from 'solid-js' +import type { JSXElement } from 'solid-js' import { type Application, Application_ContainerState, BuildStatus, type CreateWebsiteRequest, DeployType, - GetApplicationsRequest_Scope, - GetRepositoriesRequest_Scope, PortPublicationProtocol, type Repository, type Website, } from '/@/api/neoshowcase/protobuf/gateway_pb' -import { client, getRepositoryCommits } from '/@/libs/api' -import { timestampDate } from '@bufbuild/protobuf/wkt' export const buildStatusStr: Record = { [BuildStatus.QUEUED]: 'Queued', diff --git a/dashboard/src/pages/apps.tsx b/dashboard/src/pages/apps.tsx index b7c3cc8b..f36cc6f1 100644 --- a/dashboard/src/pages/apps.tsx +++ b/dashboard/src/pages/apps.tsx @@ -7,7 +7,7 @@ import { GetApplicationsRequest_Scope, GetRepositoriesRequest_Scope } from '/@/a import { styled } from '/@/components/styled-components' import type { SelectOption } from '/@/components/templates/Select' import { client, getRepositoryCommits, user } from '/@/libs/api' -import { ApplicationState, type RepositoryOrigin, useApplicationsFilter, RepoWithApp } 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' @@ -146,11 +146,10 @@ 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) @@ -169,6 +168,15 @@ export default () => { () => 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 ?? [], @@ -215,6 +223,7 @@ export default () => { leftIcon={
} rightIcon={ Date: Tue, 7 Jan 2025 19:52:12 +0900 Subject: [PATCH 4/6] feat: Improve description of sleeping state --- dashboard/src/components/templates/app/AppDeployInfo.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dashboard/src/components/templates/app/AppDeployInfo.tsx b/dashboard/src/components/templates/app/AppDeployInfo.tsx index 1c7af620..68dec3b1 100644 --- a/dashboard/src/components/templates/app/AppDeployInfo.tsx +++ b/dashboard/src/components/templates/app/AppDeployInfo.tsx @@ -160,6 +160,9 @@ const AppDeployInfo: Component<{ 現在アプリが起動していないためSSHアクセスはできません + + アプリのURLにアクセスがあった場合、自動的に起動します + From f42e134e09a13071e2c06173659e0ff75fb5268d Mon Sep 17 00:00:00 2001 From: motoki317 Date: Tue, 7 Jan 2025 20:01:08 +0900 Subject: [PATCH 5/6] feat: Improve repo count calculation --- dashboard/src/components/templates/app/AppsFilter.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dashboard/src/components/templates/app/AppsFilter.tsx b/dashboard/src/components/templates/app/AppsFilter.tsx index 21681a01..9209e2a3 100644 --- a/dashboard/src/components/templates/app/AppsFilter.tsx +++ b/dashboard/src/components/templates/app/AppsFilter.tsx @@ -48,7 +48,10 @@ const AppsFilter: Component<{ const repoCountByOrigin = (origin: RepositoryOrigin): number => { return props.allRepoWithApps .filter((repo) => repositoryURLToOrigin(repo.repo.url) === origin) - .filter((repo) => props.includeNoApp || repo.apps.length > 0).length + .filter( + (repo) => + props.includeNoApp || repo.apps.filter((app) => props.statuses.includes(applicationState(app))).length > 0, + ).length } return ( From b9691334b279f6c55fd8cc2575f4bf78cfa458b4 Mon Sep 17 00:00:00 2001 From: motoki317 Date: Tue, 7 Jan 2025 22:46:16 +0900 Subject: [PATCH 6/6] refactor: unnecessary wrapping --- dashboard/src/libs/application.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/dashboard/src/libs/application.tsx b/dashboard/src/libs/application.tsx index 4425794d..6dca55e2 100644 --- a/dashboard/src/libs/application.tsx +++ b/dashboard/src/libs/application.tsx @@ -183,19 +183,15 @@ export const useApplicationsFilter = ( includeNoApp: boolean, sort: 'asc' | 'desc', ): RepoWithApp[] => { - const filteredReposByOrigin = () => { - return repos.filter((r) => origins.includes(repositoryURLToOrigin(r.url))) ?? [] - } - const filteredApps = () => { - return apps.filter((a) => statuses.includes(applicationState(a))) ?? [] - } + 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()) { + for (const app of filteredApps) { if (!appsMap[app.repositoryId]) appsMap[app.repositoryId] = [] appsMap[app.repositoryId].push(app) } - const res = filteredReposByOrigin().reduce((acc, repo) => { + const res = filteredReposByOrigin.reduce((acc, repo) => { if (!includeNoApp && !appsMap[repo.id]) return acc acc.push({ repo, apps: appsMap[repo.id] || [] }) return acc