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={
{
}
>
-
+