Skip to content

Commit

Permalink
Merge pull request #826 from traPtitech/chores/appearance
Browse files Browse the repository at this point in the history
chores: dashboard appearance
  • Loading branch information
motoki317 authored Dec 12, 2023
2 parents feb95c1 + 039e8ff commit b33fa36
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 54 deletions.
2 changes: 1 addition & 1 deletion dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "biome check src",
"lint:apply": "biome check src --apply",
"lint:force": "biome check src --apply-unsafe",
"fix": "yarn fmt:apply && yarn lint:force",
"fix": "yarn fmt:apply && yarn lint:apply",
"typecheck": "tsc --noEmit",
"ci": "biome ci src",
"analyze": "vite build --mode analyze"
Expand Down
17 changes: 5 additions & 12 deletions dashboard/src/components/templates/app/AppDeployInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { styled } from '@macaron-css/solid'
import { Component, For, Show, createSignal } from 'solid-js'
import { Component, For, Show } from 'solid-js'
import toast from 'solid-toast'
import { Application, Build, DeployType, Repository } from '/@/api/neoshowcase/protobuf/gateway_pb'
import Badge from '/@/components/UI/Badge'
Expand Down Expand Up @@ -148,9 +148,6 @@ const AppDeployInfo: Component<{
latestBuildId: string | undefined
hasPermission: boolean
}> = (props) => {
const [mouseEnter, setMouseEnter] = createSignal(false)
const showActions = () => props.hasPermission && mouseEnter()

const stopApp = async () => {
try {
await client.stopApplication({ id: props.app.id })
Expand All @@ -163,22 +160,18 @@ const AppDeployInfo: Component<{

return (
<DeploymentContainer>
<AppStateContainer
onMouseEnter={() => setMouseEnter(true)}
onMouseLeave={() => setMouseEnter(false)}
variant={deploymentState(props.app)}
>
<AppStateContainer variant={deploymentState(props.app)}>
<div />
<AppState>
<AppStatusIcon state={deploymentState(props.app)} size={80} />
{deploymentState(props.app)}
</AppState>
<Show when={showActions()}>
<Show when={props.hasPermission}>
<ActionButtons>
<Button variants="borderError" size="small" onClick={props.startApp}>
<Button variants="primary" size="small" onClick={props.startApp}>
{props.app.running ? 'Restart App' : 'Start App'}
</Button>
<Button variants="borderError" size="small" onClick={stopApp} disabled={!props.app.running}>
<Button variants="primary" size="small" onClick={stopApp} disabled={!props.app.running}>
Stop App
</Button>
</ActionButtons>
Expand Down
4 changes: 3 additions & 1 deletion dashboard/src/components/templates/app/BuildConfigs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ interface StaticConfigProps {
}

const StaticConfigs = (props: StaticConfigProps) => {
const buildType = () => getValue(props.formStore, 'case')

return (
<>
<Field of={props.formStore} name="config.staticConfig.artifactPath" validate={[required('Enter Artifact Path')]}>
Expand All @@ -162,7 +164,7 @@ const StaticConfigs = (props: StaticConfigProps) => {
content: (
<>
<div>静的ファイルが生成されるディレクトリ</div>
<div>(Contextからの相対パス)</div>
<div>({buildType() === 'staticCmd' ? 'リポジトリルート' : 'Context'}からの相対パス)</div>
</>
),
},
Expand Down
86 changes: 54 additions & 32 deletions dashboard/src/components/templates/app/WebsiteSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import ModalDeleteConfirm from '/@/components/UI/ModalDeleteConfirm'
import { TextField } from '/@/components/UI/TextField'
import FormBox from '/@/components/layouts/FormBox'
import { systemInfo } from '/@/libs/api'
import { websiteWarnings } from '/@/libs/application'
import useModal from '/@/libs/useModal'
import { colorVars } from '/@/theme'
import { CheckBox } from '../CheckBox'
import { FormItem } from '../FormItem'
import { List } from '../List'
Expand Down Expand Up @@ -42,6 +44,18 @@ const HttpSelectContainer = styled('div', {
width: 'calc(6ch + 60px)',
},
})
const WarningsContainer = styled('div', {
base: {
display: 'flex',
flexDirection: 'column',
gap: '4px',
},
})
const WarningItem = styled('div', {
base: {
color: colorVars.semantic.accent.error,
},
})
const DeleteButtonContainer = styled('div', {
base: {
width: 'fit-content',
Expand All @@ -57,7 +71,7 @@ const AddMoreButtonContainer = styled('div', {

interface WebsiteSettingProps {
isRuntimeApp: boolean
formStore: FormStore<WebsiteSetting, undefined>
formStore: FormStore<WebsiteFormStatus, undefined>
saveWebsite?: () => void
deleteWebsite: () => void
hasPermission: boolean
Expand Down Expand Up @@ -95,16 +109,16 @@ export const WebsiteSetting = (props: WebsiteSettingProps) => {
return `${scheme}://${fqdn}${pathPrefix}`
}

const extractHost = (
const extractSubdomain = (
fqdn: string,
): {
host: string
subdomain: string
domain: PlainMessage<AvailableDomain>
} => {
const matchNonWildcardDomain = nonWildcardDomains().find((d) => fqdn === d.domain)
if (matchNonWildcardDomain !== undefined) {
return {
host: '',
subdomain: '',
domain: matchNonWildcardDomain,
}
}
Expand All @@ -114,24 +128,24 @@ export const WebsiteSetting = (props: WebsiteSettingProps) => {
const fallbackDomain = systemInfo()?.domains[0]
if (fallbackDomain === undefined) throw new Error('No domain available')
return {
host: '',
subdomain: '',
domain: fallbackDomain,
}
}
return {
host: fqdn.slice(0, -matchDomain.domain.length + 1),
subdomain: fqdn.slice(0, -matchDomain.domain.length + 1),
domain: matchDomain,
}
}

// set host and domain from fqdn on fqdn change
// set subdomain and domain from fqdn on fqdn change
createEffect(
on(
() => getValue(props.formStore, 'website.fqdn'),
(fqdn) => {
if (fqdn === undefined) return
const { host, domain } = extractHost(fqdn)
setValue(props.formStore, 'website.host', host)
const { subdomain, domain } = extractSubdomain(fqdn)
setValue(props.formStore, 'website.subdomain', subdomain)
setValue(props.formStore, 'website.domain', domain.domain)
setValue(props.formStore, 'website.authAvailable', domain.authAvailable)
if (domain.authAvailable === false) {
Expand All @@ -141,12 +155,12 @@ export const WebsiteSetting = (props: WebsiteSettingProps) => {
),
)

const resetHostAndDomain = createReaction(() => {
const resetSubdomainAndDomain = createReaction(() => {
const fqdn = getValue(props.formStore, 'website.fqdn')
if (fqdn === undefined) return
const { host, domain } = extractHost(fqdn)
reset(props.formStore, 'website.host', {
initialValue: host,
const { subdomain, domain } = extractSubdomain(fqdn)
reset(props.formStore, 'website.subdomain', {
initialValue: subdomain,
})
reset(props.formStore, 'website.domain', {
initialValue: domain.domain,
Expand All @@ -157,19 +171,19 @@ export const WebsiteSetting = (props: WebsiteSettingProps) => {
})

onMount(() => {
// Reset host and domain on first fqdn change
resetHostAndDomain(() => getValue(props.formStore, 'website.fqdn'))
// Reset subdomain and domain on first fqdn change
resetSubdomainAndDomain(() => getValue(props.formStore, 'website.fqdn'))
})

// set fqdn from host and domain on host or domain change
// set fqdn from subdomain and domain on subdomain or domain change
createEffect(
on(
[() => getValue(props.formStore, 'website.host'), () => getValue(props.formStore, 'website.domain')],
([host, domain]) => {
if (host === undefined || domain === undefined) return
[() => getValue(props.formStore, 'website.subdomain'), () => getValue(props.formStore, 'website.domain')],
([subdomain, domain]) => {
if (subdomain === undefined || domain === undefined) return
if (domain.startsWith('*')) {
// wildcard domainならhostとdomainを結合
const fqdn = `${host}${domain?.replace(/\*/g, '')}`
// wildcard domainならsubdomainとdomainを結合
const fqdn = `${subdomain}${domain?.replace(/\*/g, '')}`
setValue(props.formStore, 'website.fqdn', fqdn)
} else {
// non-wildcard domainならdomainをそのまま使う
Expand All @@ -179,6 +193,9 @@ export const WebsiteSetting = (props: WebsiteSettingProps) => {
),
)

const warnings = () =>
websiteWarnings(getValue(props.formStore, 'website.subdomain'), getValue(props.formStore, 'website.https'))

return (
<Form
of={props.formStore}
Expand Down Expand Up @@ -235,21 +252,21 @@ export const WebsiteSetting = (props: WebsiteSettingProps) => {
<URLItem>://</URLItem>
<Field
of={props.formStore}
name={'website.host'}
validate={(host) => {
if (getValue(props.formStore, 'website.domain')?.startsWith('*') && host === '') {
return 'Please Enter Hostname'
name={'website.subdomain'}
validate={(subdomain) => {
if (getValue(props.formStore, 'website.domain')?.startsWith('*') && subdomain === '') {
return 'Please Enter Subdomain Name'
}
return ''
}}
>
{(field, fieldProps) => (
<Show when={getValue(props.formStore, 'website.domain')?.startsWith('*')}>
<TextField
placeholder="example.trap.show"
placeholder="subdomain"
tooltip={{
props: {
content: 'ホスト名',
content: 'サブドメイン名',
},
}}
{...fieldProps}
Expand All @@ -265,7 +282,7 @@ export const WebsiteSetting = (props: WebsiteSettingProps) => {
<SingleSelect
tooltip={{
props: {
content: 'ドメイン',
content: 'ドメイン名',
},
}}
{...fieldProps}
Expand Down Expand Up @@ -342,6 +359,11 @@ export const WebsiteSetting = (props: WebsiteSettingProps) => {
<URLItem>/TCP</URLItem>
</Show>
</URLContainer>
<Show when={warnings().length > 0}>
<WarningsContainer>
<For each={warnings()}>{(item) => <WarningItem>{item}</WarningItem>}</For>
</WarningsContainer>
</Show>
</FormItem>
<Field of={props.formStore} name={'website.authentication'} type="number">
{(field, fieldProps) => (
Expand Down Expand Up @@ -469,12 +491,12 @@ export const WebsiteSetting = (props: WebsiteSettingProps) => {
}

type FQDN = {
host: string
subdomain: string
domain: PlainMessage<AvailableDomain>['domain']
authAvailable: PlainMessage<AvailableDomain>['authAvailable']
}

export type WebsiteSetting =
export type WebsiteFormStatus =
| {
/**
* - `noChange`: 既存の設定を変更していない
Expand All @@ -493,7 +515,7 @@ export type WebsiteSetting =
}

export type WebsiteSettingForm = {
websites: WebsiteSetting[]
websites: WebsiteFormStatus[]
}

export const newWebsite = (): PlainMessage<CreateWebsiteRequest> => ({
Expand All @@ -508,7 +530,7 @@ export const newWebsite = (): PlainMessage<CreateWebsiteRequest> => ({

interface WebsiteSettingsProps {
isRuntimeApp: boolean
formStores: FormStore<WebsiteSetting, undefined>[]
formStores: FormStore<WebsiteFormStatus, undefined>[]
addWebsite: () => void
deleteWebsiteForm: (index: number) => void
applyChanges: () => void
Expand Down
12 changes: 12 additions & 0 deletions dashboard/src/libs/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ export const getWebsiteURL = (website: PlainMessage<Website | CreateWebsiteReque
return `${scheme}://${website.fqdn}${website.pathPrefix}`
}

export const websiteWarnings = (subdomain: string | undefined, isHTTPS: boolean | undefined): string[] => {
const warnings = []
if (subdomain?.includes('_')) {
warnings.push('アンダースコア「_」を含むホスト名は非推奨です。将来非対応になる可能性があります。')
}
const labels = subdomain?.split('.')
if (isHTTPS && labels && labels.length >= 2) {
warnings.push('このホスト名では専用の証明書が取得されます。可能な限りホストのラベル数は少なくしてください。')
}
return warnings
}

export const extractRepositoryNameFromURL = (url: string): string => {
const segments = url.split('/')
const lastSegment = segments.pop() || segments.pop() // 末尾のスラッシュを除去
Expand Down
6 changes: 3 additions & 3 deletions dashboard/src/pages/apps/[id]/settings/urls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Show, createResource } from 'solid-js'
import toast from 'solid-toast'
import { DeployType } from '/@/api/neoshowcase/protobuf/gateway_pb'
import { DataTable } from '/@/components/layouts/DataTable'
import { WebsiteSetting, WebsiteSettings, newWebsite } from '/@/components/templates/app/WebsiteSettings'
import { WebsiteFormStatus, WebsiteSettings, newWebsite } from '/@/components/templates/app/WebsiteSettings'
import { client, handleAPIError } from '/@/libs/api'
import { useApplicationData } from '/@/routes'

Expand All @@ -14,7 +14,7 @@ export default () => {
() => app()?.websites,
(websites) => {
return websites.map((website) => {
const form = createFormStore<WebsiteSetting>({
const form = createFormStore<WebsiteFormStatus>({
initialValues: {
state: 'noChange',
website: structuredClone(website),
Expand All @@ -25,7 +25,7 @@ export default () => {
},
)
const addWebsiteForm = () => {
const form = createFormStore<WebsiteSetting>({
const form = createFormStore<WebsiteFormStatus>({
initialValues: {
state: 'added',
website: newWebsite(),
Expand Down
10 changes: 5 additions & 5 deletions dashboard/src/pages/apps/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { List } from '/@/components/templates/List'
import { Nav } from '/@/components/templates/Nav'
import { AppGeneralConfig, AppGeneralForm } from '/@/components/templates/app/AppGeneralConfig'
import { BuildConfigForm, BuildConfigs, configToForm, formToConfig } from '/@/components/templates/app/BuildConfigs'
import { WebsiteSetting, newWebsite } from '/@/components/templates/app/WebsiteSettings'
import { WebsiteFormStatus, WebsiteSetting, newWebsite } from '/@/components/templates/app/WebsiteSettings'
import ReposFilter from '/@/components/templates/repo/ReposFilter'
import { client, handleAPIError, systemInfo } from '/@/libs/api'
import { Provider, providerToIcon, repositoryURLToProvider } from '/@/libs/application'
Expand Down Expand Up @@ -403,13 +403,13 @@ const AddMoreButtonContainer = styled('div', {

const WebsiteStep: Component<{
isRuntimeApp: boolean
websiteForms: Accessor<FormStore<WebsiteSetting, undefined>[]>
setWebsiteForms: Setter<FormStore<WebsiteSetting, undefined>[]>
websiteForms: Accessor<FormStore<WebsiteFormStatus, undefined>[]>
setWebsiteForms: Setter<FormStore<WebsiteFormStatus, undefined>[]>
backToGeneralStep: () => void
submit: () => Promise<void>
}> = (props) => {
const addWebsiteForm = () => {
const form = createFormStore<WebsiteSetting>({
const form = createFormStore<WebsiteFormStatus>({
initialValues: {
state: 'added',
website: newWebsite(),
Expand Down Expand Up @@ -555,7 +555,7 @@ export default () => {
setValue(createAppForm, 'repositoryId', repo()?.id)
})

const [websiteForms, setWebsiteForms] = createSignal<FormStore<WebsiteSetting, undefined>[]>([])
const [websiteForms, setWebsiteForms] = createSignal<FormStore<WebsiteFormStatus, undefined>[]>([])

// TODO: ブラウザバック時のrepositoryIDの設定

Expand Down

0 comments on commit b33fa36

Please sign in to comment.